Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
S
Son Of Anton
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Administrator
Son Of Anton
Commits
3c51ab07
Commit
3c51ab07
authored
Apr 10, 2026
by
Mahmoud Aglan
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
lol
parent
1cd1b7d6
Changes
8
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
380 additions
and
699 deletions
+380
-699
main.py
backend/main.py
+1
-2
models.py
backend/models.py
+3
-1
chat_routes.py
backend/routes/chat_routes.py
+41
-13
gitlab_routes.py
backend/routes/gitlab_routes.py
+21
-158
generation_manager.py
backend/services/generation_manager.py
+8
-13
package.json
frontend/package.json
+0
-5
api.js
frontend/src/api.js
+121
-230
ChatView.jsx
frontend/src/components/ChatView.jsx
+185
-277
No files found.
backend/main.py
View file @
3c51ab07
...
@@ -42,14 +42,13 @@ def _run_migrations():
...
@@ -42,14 +42,13 @@ def _run_migrations():
conn
.
execute
(
text
(
"ALTER TABLE chats ADD COLUMN reasoning_budget INTEGER DEFAULT 0"
))
conn
.
execute
(
text
(
"ALTER TABLE chats ADD COLUMN reasoning_budget INTEGER DEFAULT 0"
))
if
"linked_repo_id"
not
in
columns
:
if
"linked_repo_id"
not
in
columns
:
conn
.
execute
(
text
(
"ALTER TABLE chats ADD COLUMN linked_repo_id VARCHAR(36)"
))
conn
.
execute
(
text
(
"ALTER TABLE chats ADD COLUMN linked_repo_id VARCHAR(36)"
))
if
"linked_repo_branch"
not
in
columns
:
conn
.
execute
(
text
(
"ALTER TABLE chats ADD COLUMN linked_repo_branch VARCHAR(100)"
))
conn
.
commit
()
conn
.
commit
()
if
"chat_attachments"
not
in
existing_tables
:
if
"chat_attachments"
not
in
existing_tables
:
from
backend.models
import
ChatAttachment
from
backend.models
import
ChatAttachment
ChatAttachment
.
__table__
.
create
(
bind
=
engine
,
checkfirst
=
True
)
ChatAttachment
.
__table__
.
create
(
bind
=
engine
,
checkfirst
=
True
)
# Create user_permissions table if missing
if
"user_permissions"
not
in
existing_tables
:
if
"user_permissions"
not
in
existing_tables
:
from
backend.models
import
UserPermissions
from
backend.models
import
UserPermissions
UserPermissions
.
__table__
.
create
(
bind
=
engine
,
checkfirst
=
True
)
UserPermissions
.
__table__
.
create
(
bind
=
engine
,
checkfirst
=
True
)
...
...
backend/models.py
View file @
3c51ab07
...
@@ -54,6 +54,7 @@ class UserPermissions(Base):
...
@@ -54,6 +54,7 @@ class UserPermissions(Base):
unique
=
True
,
nullable
=
False
,
index
=
True
,
unique
=
True
,
nullable
=
False
,
index
=
True
,
)
)
# Feature access
can_use_web_search
=
Column
(
Boolean
,
default
=
False
)
can_use_web_search
=
Column
(
Boolean
,
default
=
False
)
can_use_ui_design
=
Column
(
Boolean
,
default
=
False
)
can_use_ui_design
=
Column
(
Boolean
,
default
=
False
)
can_use_knowledge_base
=
Column
(
Boolean
,
default
=
True
)
can_use_knowledge_base
=
Column
(
Boolean
,
default
=
True
)
...
@@ -62,8 +63,10 @@ class UserPermissions(Base):
...
@@ -62,8 +63,10 @@ class UserPermissions(Base):
can_export_pptx
=
Column
(
Boolean
,
default
=
True
)
can_export_pptx
=
Column
(
Boolean
,
default
=
True
)
can_export_docx
=
Column
(
Boolean
,
default
=
True
)
can_export_docx
=
Column
(
Boolean
,
default
=
True
)
# Model access — "all" or comma-separated model IDs
allowed_models
=
Column
(
Text
,
default
=
"eu.anthropic.claude-haiku-4-5-20251001-v1:0"
)
allowed_models
=
Column
(
Text
,
default
=
"eu.anthropic.claude-haiku-4-5-20251001-v1:0"
)
# Limits (0 = unlimited for count-based limits)
max_tokens_cap
=
Column
(
Integer
,
default
=
4096
)
max_tokens_cap
=
Column
(
Integer
,
default
=
4096
)
max_reasoning_budget
=
Column
(
Integer
,
default
=
0
)
max_reasoning_budget
=
Column
(
Integer
,
default
=
0
)
max_chats
=
Column
(
Integer
,
default
=
50
)
max_chats
=
Column
(
Integer
,
default
=
50
)
...
@@ -87,7 +90,6 @@ class Chat(Base):
...
@@ -87,7 +90,6 @@ class Chat(Base):
model
=
Column
(
String
(
100
),
default
=
"eu.anthropic.claude-opus-4-6-v1"
)
model
=
Column
(
String
(
100
),
default
=
"eu.anthropic.claude-opus-4-6-v1"
)
knowledge_base_id
=
Column
(
String
(
36
),
nullable
=
True
)
knowledge_base_id
=
Column
(
String
(
36
),
nullable
=
True
)
linked_repo_id
=
Column
(
String
(
36
),
nullable
=
True
)
linked_repo_id
=
Column
(
String
(
36
),
nullable
=
True
)
linked_repo_branch
=
Column
(
String
(
100
),
nullable
=
True
)
max_tokens
=
Column
(
Integer
,
default
=
4096
)
max_tokens
=
Column
(
Integer
,
default
=
4096
)
reasoning_budget
=
Column
(
Integer
,
default
=
0
)
reasoning_budget
=
Column
(
Integer
,
default
=
0
)
created_at
=
Column
(
DateTime
,
default
=
datetime
.
utcnow
)
created_at
=
Column
(
DateTime
,
default
=
datetime
.
utcnow
)
...
...
backend/routes/chat_routes.py
View file @
3c51ab07
...
@@ -28,7 +28,6 @@ class CreateChatBody(BaseModel):
...
@@ -28,7 +28,6 @@ class CreateChatBody(BaseModel):
model
:
str
=
"eu.anthropic.claude-opus-4-6-v1"
model
:
str
=
"eu.anthropic.claude-opus-4-6-v1"
knowledge_base_id
:
Optional
[
str
]
=
None
knowledge_base_id
:
Optional
[
str
]
=
None
linked_repo_id
:
Optional
[
str
]
=
None
linked_repo_id
:
Optional
[
str
]
=
None
linked_repo_branch
:
Optional
[
str
]
=
None
max_tokens
:
int
=
4096
max_tokens
:
int
=
4096
reasoning_budget
:
int
=
0
reasoning_budget
:
int
=
0
...
@@ -40,7 +39,6 @@ class UpdateChatBody(BaseModel):
...
@@ -40,7 +39,6 @@ class UpdateChatBody(BaseModel):
reasoning_budget
:
Optional
[
int
]
=
None
reasoning_budget
:
Optional
[
int
]
=
None
knowledge_base_id
:
Optional
[
str
]
=
None
knowledge_base_id
:
Optional
[
str
]
=
None
linked_repo_id
:
Optional
[
str
]
=
None
linked_repo_id
:
Optional
[
str
]
=
None
linked_repo_branch
:
Optional
[
str
]
=
None
class
SendMessageBody
(
BaseModel
):
class
SendMessageBody
(
BaseModel
):
...
@@ -87,7 +85,6 @@ def create_chat(body: CreateChatBody, user: User = Depends(get_current_user), db
...
@@ -87,7 +85,6 @@ def create_chat(body: CreateChatBody, user: User = Depends(get_current_user), db
model
=
check_model_allowed
(
user
.
id
,
body
.
model
,
db
),
model
=
check_model_allowed
(
user
.
id
,
body
.
model
,
db
),
knowledge_base_id
=
body
.
knowledge_base_id
or
None
,
knowledge_base_id
=
body
.
knowledge_base_id
or
None
,
linked_repo_id
=
body
.
linked_repo_id
or
None
,
linked_repo_id
=
body
.
linked_repo_id
or
None
,
linked_repo_branch
=
body
.
linked_repo_branch
or
None
,
max_tokens
=
min
(
body
.
max_tokens
,
perms
.
get
(
"max_tokens_cap"
,
4096
)),
max_tokens
=
min
(
body
.
max_tokens
,
perms
.
get
(
"max_tokens_cap"
,
4096
)),
reasoning_budget
=
min
(
body
.
reasoning_budget
,
perms
.
get
(
"max_reasoning_budget"
,
0
)),
reasoning_budget
=
min
(
body
.
reasoning_budget
,
perms
.
get
(
"max_reasoning_budget"
,
0
)),
)
)
...
@@ -119,8 +116,6 @@ def update_chat(chat_id: str, body: UpdateChatBody, user: User = Depends(get_cur
...
@@ -119,8 +116,6 @@ def update_chat(chat_id: str, body: UpdateChatBody, user: User = Depends(get_cur
if
body
.
linked_repo_id
and
not
perms
.
get
(
"can_use_gitlab"
):
if
body
.
linked_repo_id
and
not
perms
.
get
(
"can_use_gitlab"
):
raise
HTTPException
(
403
,
"GitLab not enabled."
)
raise
HTTPException
(
403
,
"GitLab not enabled."
)
chat
.
linked_repo_id
=
body
.
linked_repo_id
or
None
chat
.
linked_repo_id
=
body
.
linked_repo_id
or
None
if
body
.
linked_repo_branch
is
not
None
:
chat
.
linked_repo_branch
=
body
.
linked_repo_branch
or
None
db
.
commit
()
db
.
commit
()
return
_chat_dict
(
chat
,
db
)
return
_chat_dict
(
chat
,
db
)
...
@@ -210,6 +205,11 @@ async def commit_from_chat(
...
@@ -210,6 +205,11 @@ async def commit_from_chat(
user
:
User
=
Depends
(
get_current_user
),
user
:
User
=
Depends
(
get_current_user
),
db
:
Session
=
Depends
(
get_db
),
db
:
Session
=
Depends
(
get_db
),
):
):
"""
Commit files from chat to linked GitLab repo.
Auto-detects whether each file should be 'create' or 'update'
by checking the repo tree, so it never fails on wrong action type.
"""
check_feature
(
user
.
id
,
"use_gitlab"
,
db
)
check_feature
(
user
.
id
,
"use_gitlab"
,
db
)
chat
=
db
.
query
(
Chat
)
.
filter
(
Chat
.
id
==
chat_id
,
Chat
.
user_id
==
user
.
id
)
.
first
()
chat
=
db
.
query
(
Chat
)
.
filter
(
Chat
.
id
==
chat_id
,
Chat
.
user_id
==
user
.
id
)
.
first
()
...
@@ -226,44 +226,73 @@ async def commit_from_chat(
...
@@ -226,44 +226,73 @@ async def commit_from_chat(
if
not
settings
or
not
settings
.
is_active
:
if
not
settings
or
not
settings
.
is_active
:
raise
HTTPException
(
400
,
"GitLab not configured"
)
raise
HTTPException
(
400
,
"GitLab not configured"
)
# ── Fetch repo tree to know which files already exist ──
existing_paths
=
set
()
existing_paths
=
set
()
try
:
try
:
tree
=
await
gitlab_service
.
get_tree
(
tree
=
await
gitlab_service
.
get_tree
(
settings
.
gitlab_url
,
settings
.
gitlab_token
,
settings
.
gitlab_url
,
repo
.
gitlab_project_id
,
ref
=
body
.
branch
,
recursive
=
True
,
settings
.
gitlab_token
,
repo
.
gitlab_project_id
,
ref
=
body
.
branch
,
recursive
=
True
,
)
)
existing_paths
=
{
item
[
"path"
]
for
item
in
tree
if
item
[
"type"
]
==
"blob"
}
existing_paths
=
{
item
[
"path"
]
for
item
in
tree
if
item
[
"type"
]
==
"blob"
}
except
Exception
:
except
Exception
:
# If tree fetch fails (empty repo, network issue, etc.),
# we'll try all as "create" since we can't know what exists
pass
pass
# ── Build actions with auto-detected create/update ──
actions
=
[]
actions
=
[]
for
f
in
body
.
files
:
for
f
in
body
.
files
:
file_path
=
f
.
get
(
"file_path"
,
""
)
file_path
=
f
.
get
(
"file_path"
,
""
)
content
=
f
.
get
(
"content"
,
""
)
content
=
f
.
get
(
"content"
,
""
)
requested_action
=
f
.
get
(
"action"
,
"auto"
)
requested_action
=
f
.
get
(
"action"
,
"auto"
)
if
not
file_path
or
not
content
:
if
not
file_path
or
not
content
:
continue
continue
file_exists
=
file_path
in
existing_paths
file_exists
=
file_path
in
existing_paths
# Smart action resolution
if
requested_action
in
(
"auto"
,
"upsert"
):
if
requested_action
in
(
"auto"
,
"upsert"
):
# Auto-detect: update if exists, create if not
actual_action
=
"update"
if
file_exists
else
"create"
actual_action
=
"update"
if
file_exists
else
"create"
elif
requested_action
==
"update"
and
not
file_exists
:
elif
requested_action
==
"update"
and
not
file_exists
:
# User said update but file doesn't exist → create instead
actual_action
=
"create"
actual_action
=
"create"
elif
requested_action
==
"create"
and
file_exists
:
elif
requested_action
==
"create"
and
file_exists
:
# User said create but file already exists → update instead
actual_action
=
"update"
actual_action
=
"update"
else
:
else
:
actual_action
=
requested_action
actual_action
=
requested_action
actions
.
append
({
"action"
:
actual_action
,
"file_path"
:
file_path
,
"content"
:
content
})
actions
.
append
({
"action"
:
actual_action
,
"file_path"
:
file_path
,
"content"
:
content
,
})
if
not
actions
:
if
not
actions
:
raise
HTTPException
(
400
,
"No valid files to commit"
)
raise
HTTPException
(
400
,
"No valid files to commit"
)
try
:
try
:
result
=
await
gitlab_service
.
commit_files
(
result
=
await
gitlab_service
.
commit_files
(
settings
.
gitlab_url
,
settings
.
gitlab_token
,
settings
.
gitlab_url
,
repo
.
gitlab_project_id
,
body
.
branch
,
body
.
commit_message
,
actions
,
settings
.
gitlab_token
,
repo
.
gitlab_project_id
,
body
.
branch
,
body
.
commit_message
,
actions
,
)
)
gen_manager
.
invalidate_repo_cache
(
repo
.
id
)
gen_manager
.
invalidate_repo_cache
(
repo
.
id
)
return
{
"ok"
:
True
,
"commit"
:
result
,
"files_committed"
:
len
(
actions
)}
return
{
"ok"
:
True
,
"commit"
:
result
,
"files_committed"
:
len
(
actions
),
}
except
gitlab_service
.
GitLabError
as
e
:
except
gitlab_service
.
GitLabError
as
e
:
raise
HTTPException
(
e
.
status_code
,
f
"Commit failed: {e.detail}"
)
raise
HTTPException
(
e
.
status_code
,
f
"Commit failed: {e.detail}"
)
...
@@ -285,7 +314,6 @@ def _chat_dict(c, db=None):
...
@@ -285,7 +314,6 @@ def _chat_dict(c, db=None):
"id"
:
c
.
id
,
"title"
:
c
.
title
,
"model"
:
c
.
model
,
"id"
:
c
.
id
,
"title"
:
c
.
title
,
"model"
:
c
.
model
,
"knowledge_base_id"
:
c
.
knowledge_base_id
,
"knowledge_base_id"
:
c
.
knowledge_base_id
,
"linked_repo_id"
:
c
.
linked_repo_id
,
"linked_repo_id"
:
c
.
linked_repo_id
,
"linked_repo_branch"
:
getattr
(
c
,
"linked_repo_branch"
,
None
)
or
None
,
"max_tokens"
:
c
.
max_tokens
or
4096
,
"max_tokens"
:
c
.
max_tokens
or
4096
,
"reasoning_budget"
:
c
.
reasoning_budget
or
0
,
"reasoning_budget"
:
c
.
reasoning_budget
or
0
,
"created_at"
:
str
(
c
.
created_at
),
"created_at"
:
str
(
c
.
created_at
),
...
...
backend/routes/gitlab_routes.py
View file @
3c51ab07
"""
"""
GitLab CE integration routes — superadmin
+ user-facing
.
GitLab CE integration routes — superadmin
only
.
Son of Anton v4.2.0
Son of Anton v4.2.0
"""
"""
...
@@ -13,9 +13,9 @@ from fastapi import APIRouter, Depends, HTTPException, Query
...
@@ -13,9 +13,9 @@ from fastapi import APIRouter, Depends, HTTPException, Query
from
sqlalchemy.orm
import
Session
from
sqlalchemy.orm
import
Session
from
backend.database
import
get_db
from
backend.database
import
get_db
from
backend.models
import
User
,
GitLabSettings
,
LinkedRepo
,
PendingAction
,
KnowledgeBase
,
KnowledgeDocument
from
backend.models
import
User
,
GitLabSettings
,
LinkedRepo
,
PendingAction
from
backend.auth
import
require_superadmin
,
get_current_user
,
check_feature
from
backend.auth
import
require_superadmin
from
backend.services
import
gitlab_service
,
code_analyzer
,
rag_service
from
backend.services
import
gitlab_service
,
code_analyzer
router
=
APIRouter
()
router
=
APIRouter
()
...
@@ -64,10 +64,6 @@ class ActionBody(BaseModel):
...
@@ -64,10 +64,6 @@ class ActionBody(BaseModel):
title
:
str
=
""
title
:
str
=
""
payload
:
str
payload
:
str
class
CreateKBFromRepoBody
(
BaseModel
):
branch
:
Optional
[
str
]
=
None
name
:
Optional
[
str
]
=
None
def
_get_settings
(
db
:
Session
)
->
GitLabSettings
:
def
_get_settings
(
db
:
Session
)
->
GitLabSettings
:
s
=
db
.
query
(
GitLabSettings
)
.
first
()
s
=
db
.
query
(
GitLabSettings
)
.
first
()
...
@@ -350,10 +346,13 @@ async def create_branch(repo_id: str, body: BranchBody, admin: User = Depends(re
...
@@ -350,10 +346,13 @@ async def create_branch(repo_id: str, body: BranchBody, admin: User = Depends(re
@
router
.
post
(
"/repos/{repo_id}/commit"
)
@
router
.
post
(
"/repos/{repo_id}/commit"
)
async
def
commit_code
(
repo_id
:
str
,
body
:
CommitBody
,
admin
:
User
=
Depends
(
require_superadmin
),
db
:
Session
=
Depends
(
get_db
)):
async
def
commit_code
(
repo_id
:
str
,
body
:
CommitBody
,
admin
:
User
=
Depends
(
require_superadmin
),
db
:
Session
=
Depends
(
get_db
)):
"""Commit multiple files. Auto-detects create vs update per file."""
"""
Commit multiple files. Auto-detects create vs update per file.
"""
s
=
_get_settings
(
db
)
s
=
_get_settings
(
db
)
repo
=
_get_repo
(
repo_id
,
db
)
repo
=
_get_repo
(
repo_id
,
db
)
# Fetch tree to know which files exist
existing_paths
=
set
()
existing_paths
=
set
()
try
:
try
:
tree
=
await
gitlab_service
.
get_tree
(
tree
=
await
gitlab_service
.
get_tree
(
...
@@ -369,9 +368,12 @@ async def commit_code(repo_id: str, body: CommitBody, admin: User = Depends(requ
...
@@ -369,9 +368,12 @@ async def commit_code(repo_id: str, body: CommitBody, admin: User = Depends(requ
file_path
=
a
.
get
(
"file_path"
,
""
)
file_path
=
a
.
get
(
"file_path"
,
""
)
content
=
a
.
get
(
"content"
,
""
)
content
=
a
.
get
(
"content"
,
""
)
requested
=
a
.
get
(
"action"
,
"auto"
)
requested
=
a
.
get
(
"action"
,
"auto"
)
if
not
file_path
:
if
not
file_path
:
continue
continue
file_exists
=
file_path
in
existing_paths
file_exists
=
file_path
in
existing_paths
if
requested
in
(
"auto"
,
"upsert"
):
if
requested
in
(
"auto"
,
"upsert"
):
actual
=
"update"
if
file_exists
else
"create"
actual
=
"update"
if
file_exists
else
"create"
elif
requested
==
"update"
and
not
file_exists
:
elif
requested
==
"update"
and
not
file_exists
:
...
@@ -380,7 +382,12 @@ async def commit_code(repo_id: str, body: CommitBody, admin: User = Depends(requ
...
@@ -380,7 +382,12 @@ async def commit_code(repo_id: str, body: CommitBody, admin: User = Depends(requ
actual
=
"update"
actual
=
"update"
else
:
else
:
actual
=
requested
actual
=
requested
resolved_actions
.
append
({
"action"
:
actual
,
"file_path"
:
file_path
,
"content"
:
content
})
resolved_actions
.
append
({
"action"
:
actual
,
"file_path"
:
file_path
,
"content"
:
content
,
})
if
not
resolved_actions
:
if
not
resolved_actions
:
raise
HTTPException
(
400
,
"No valid files to commit"
)
raise
HTTPException
(
400
,
"No valid files to commit"
)
...
@@ -402,10 +409,13 @@ async def commit_single(
...
@@ -402,10 +409,13 @@ async def commit_single(
admin
:
User
=
Depends
(
require_superadmin
),
admin
:
User
=
Depends
(
require_superadmin
),
db
:
Session
=
Depends
(
get_db
),
db
:
Session
=
Depends
(
get_db
),
):
):
"""Commit a single file. Auto-detects create vs update."""
"""
Commit a single file. Auto-detects create vs update.
"""
s
=
_get_settings
(
db
)
s
=
_get_settings
(
db
)
repo
=
_get_repo
(
repo_id
,
db
)
repo
=
_get_repo
(
repo_id
,
db
)
# Auto-detect whether file exists
action
=
body
.
action
action
=
body
.
action
if
action
in
(
"update"
,
"create"
,
"auto"
):
if
action
in
(
"update"
,
"create"
,
"auto"
):
try
:
try
:
...
@@ -537,153 +547,6 @@ def reject_action(action_id: str, admin: User = Depends(require_superadmin), db:
...
@@ -537,153 +547,6 @@ def reject_action(action_id: str, admin: User = Depends(require_superadmin), db:
return
{
"ok"
:
True
}
return
{
"ok"
:
True
}
# ═══════════════════════════════════════════════════
# USER-FACING ENDPOINTS (permission-gated, not superadmin-only)
# ═══════════════════════════════════════════════════
@
router
.
get
(
"/user/repos"
)
def
user_list_repos
(
user
:
User
=
Depends
(
get_current_user
),
db
:
Session
=
Depends
(
get_db
)):
"""List linked repos — available to any user with can_use_gitlab."""
check_feature
(
user
.
id
,
"use_gitlab"
,
db
)
repos
=
db
.
query
(
LinkedRepo
)
.
order_by
(
LinkedRepo
.
created_at
.
desc
())
.
all
()
return
[
_repo_dict
(
r
)
for
r
in
repos
]
@
router
.
get
(
"/user/repos/{repo_id}/branches"
)
async
def
user_list_branches
(
repo_id
:
str
,
user
:
User
=
Depends
(
get_current_user
),
db
:
Session
=
Depends
(
get_db
)):
"""List branches for a linked repo — available to any user with can_use_gitlab."""
check_feature
(
user
.
id
,
"use_gitlab"
,
db
)
s
=
_get_settings
(
db
)
repo
=
_get_repo
(
repo_id
,
db
)
try
:
branches
=
await
gitlab_service
.
list_branches
(
s
.
gitlab_url
,
s
.
gitlab_token
,
repo
.
gitlab_project_id
)
return
branches
except
gitlab_service
.
GitLabError
as
e
:
raise
HTTPException
(
e
.
status_code
,
e
.
detail
)
@
router
.
post
(
"/user/repos/{repo_id}/create-kb"
)
async
def
user_create_kb_from_repo
(
repo_id
:
str
,
body
:
CreateKBFromRepoBody
,
user
:
User
=
Depends
(
get_current_user
),
db
:
Session
=
Depends
(
get_db
),
):
"""Create a Knowledge Base from all code files in a repo. Runs in background."""
check_feature
(
user
.
id
,
"use_gitlab"
,
db
)
check_feature
(
user
.
id
,
"use_knowledge_base"
,
db
)
repo
=
_get_repo
(
repo_id
,
db
)
s
=
_get_settings
(
db
)
branch
=
body
.
branch
or
repo
.
default_branch
kb_name
=
body
.
name
or
f
"Repo: {repo.name} ({branch})"
kb
=
KnowledgeBase
(
user_id
=
user
.
id
,
name
=
kb_name
,
description
=
f
"Auto-generated from {repo.path_with_namespace} branch:{branch} — processing..."
,
)
db
.
add
(
kb
)
db
.
commit
()
db
.
refresh
(
kb
)
rag_service
.
create_collection
(
kb
.
id
)
asyncio
.
create_task
(
_kb_from_repo_background
(
kb
.
id
,
s
.
gitlab_url
,
s
.
gitlab_token
,
repo
.
gitlab_project_id
,
branch
,
repo
.
name
,
repo
.
path_with_namespace
,
))
return
{
"kb_id"
:
kb
.
id
,
"name"
:
kb_name
,
"status"
:
"processing"
}
async
def
_kb_from_repo_background
(
kb_id
,
gitlab_url
,
gitlab_token
,
project_id
,
branch
,
repo_name
,
repo_path
):
"""Background task: load every code file from repo into a KB."""
from
backend.database
import
SessionLocal
as
BgSession
db
=
BgSession
()
try
:
result
=
await
gitlab_service
.
load_project_files
(
gitlab_url
,
gitlab_token
,
project_id
,
ref
=
branch
,
)
files
=
result
.
get
(
"files"
,
[])
if
not
files
:
kb
=
db
.
query
(
KnowledgeBase
)
.
filter
(
KnowledgeBase
.
id
==
kb_id
)
.
first
()
if
kb
:
kb
.
description
=
f
"From {repo_path} ({branch}) — no files found"
db
.
commit
()
return
total_docs
=
0
total_chunks
=
0
total_chars
=
0
for
f
in
files
:
content
=
f
.
get
(
"content"
,
""
)
path
=
f
.
get
(
"path"
,
"unknown"
)
if
not
content
or
content
.
startswith
(
"["
):
continue
chunks
=
_chunk_for_kb
(
content
,
chunk_size
=
3000
,
overlap
=
300
)
if
not
chunks
:
continue
rag_service
.
add_documents
(
collection_id
=
kb_id
,
documents
=
chunks
,
metadatas
=
[{
"filename"
:
path
,
"chunk_index"
:
i
}
for
i
in
range
(
len
(
chunks
))],
)
doc
=
KnowledgeDocument
(
knowledge_base_id
=
kb_id
,
filename
=
path
,
file_size
=
len
(
content
),
chunk_count
=
len
(
chunks
),
)
db
.
add
(
doc
)
total_docs
+=
1
total_chunks
+=
len
(
chunks
)
total_chars
+=
len
(
content
)
kb
=
db
.
query
(
KnowledgeBase
)
.
filter
(
KnowledgeBase
.
id
==
kb_id
)
.
first
()
if
kb
:
kb
.
document_count
=
total_docs
kb
.
chunk_count
=
total_chunks
kb
.
total_characters
=
total_chars
kb
.
description
=
f
"From {repo_path} ({branch}) — {total_docs} files, {total_chunks} chunks"
db
.
commit
()
print
(
f
" ✅ KB from repo done: {repo_name} — {total_docs} files, {total_chunks} chunks, {total_chars:,} chars"
)
except
Exception
as
e
:
print
(
f
" ❌ KB from repo failed for {repo_name}: {e}"
)
try
:
kb
=
db
.
query
(
KnowledgeBase
)
.
filter
(
KnowledgeBase
.
id
==
kb_id
)
.
first
()
if
kb
:
kb
.
description
=
f
"From {repo_path} ({branch}) — ERROR: {str(e)[:200]}"
db
.
commit
()
except
Exception
:
pass
finally
:
db
.
close
()
def
_chunk_for_kb
(
text
,
chunk_size
=
3000
,
overlap
=
300
):
chunks
=
[]
start
=
0
while
start
<
len
(
text
):
end
=
start
+
chunk_size
if
end
<
len
(
text
):
for
sep
in
[
"
\n\n
"
,
"
\n
"
,
". "
,
" "
]:
pos
=
text
.
rfind
(
sep
,
start
+
chunk_size
//
2
,
end
)
if
pos
>
start
:
end
=
pos
+
len
(
sep
)
break
chunk
=
text
[
start
:
end
]
.
strip
()
if
chunk
:
chunks
.
append
(
chunk
)
start
=
end
-
overlap
if
end
<
len
(
text
)
else
end
return
chunks
# ═══════════════════════════════════════════════════
# ═══════════════════════════════════════════════════
# Helpers
# Helpers
# ═══════════════════════════════════════════════════
# ═══════════════════════════════════════════════════
...
...
backend/services/generation_manager.py
View file @
3c51ab07
"""
"""
Background generation manager — v4.
2.0 with web search + branch-aware repo context
.
Background generation manager — v4.
1.0 with web search
.
"""
"""
import
asyncio
import
asyncio
...
@@ -47,9 +47,6 @@ class GenerationManager:
...
@@ -47,9 +47,6 @@ class GenerationManager:
state
=
self
.
_active
.
get
(
chat_id
)
state
=
self
.
_active
.
get
(
chat_id
)
return
state
is
not
None
and
not
state
.
done
.
is_set
()
return
state
is
not
None
and
not
state
.
done
.
is_set
()
def
get_state
(
self
,
chat_id
:
str
)
->
Optional
[
GenerationState
]:
return
self
.
_active
.
get
(
chat_id
)
def
start
(
self
,
chat_id
,
user_id
,
content
,
model
,
max_tokens
,
reasoning_budget
,
knowledge_base_id
,
attachment_ids
,
web_search
=
False
):
def
start
(
self
,
chat_id
,
user_id
,
content
,
model
,
max_tokens
,
reasoning_budget
,
knowledge_base_id
,
attachment_ids
,
web_search
=
False
):
old
=
self
.
_active
.
get
(
chat_id
)
old
=
self
.
_active
.
get
(
chat_id
)
if
old
and
not
old
.
done
.
is_set
():
if
old
and
not
old
.
done
.
is_set
():
...
@@ -82,27 +79,25 @@ class GenerationManager:
...
@@ -82,27 +79,25 @@ class GenerationManager:
if
not
repo
:
return
None
if
not
repo
:
return
None
settings
=
db
.
query
(
GitLabSettings
)
.
first
()
settings
=
db
.
query
(
GitLabSettings
)
.
first
()
if
not
settings
or
not
settings
.
is_active
:
return
None
if
not
settings
or
not
settings
.
is_active
:
return
None
branch
=
getattr
(
chat
,
'linked_repo_branch'
,
None
)
or
repo
.
default_branch
try
:
try
:
tree
=
_get_tree_cache
(
repo
.
id
,
branch
)
tree
=
_get_tree_cache
(
repo
.
id
,
repo
.
default_
branch
)
if
tree
is
None
:
if
tree
is
None
:
tree
=
await
gitlab_service
.
get_tree
(
settings
.
gitlab_url
,
settings
.
gitlab_token
,
repo
.
gitlab_project_id
,
ref
=
branch
,
recursive
=
True
)
tree
=
await
gitlab_service
.
get_tree
(
settings
.
gitlab_url
,
settings
.
gitlab_token
,
repo
.
gitlab_project_id
,
ref
=
repo
.
default_
branch
,
recursive
=
True
)
_set_tree_cache
(
repo
.
id
,
branch
,
tree
)
_set_tree_cache
(
repo
.
id
,
repo
.
default_
branch
,
tree
)
prev
=
_chat_file_history
.
get
(
chat
.
id
,
set
())
prev
=
_chat_file_history
.
get
(
chat
.
id
,
set
())
result
=
await
gitlab_service
.
load_smart_files
(
settings
.
gitlab_url
,
settings
.
gitlab_token
,
repo
.
gitlab_project_id
,
ref
=
branch
,
tree
=
tree
,
user_query
=
user_query
,
previous_files
=
prev
)
result
=
await
gitlab_service
.
load_smart_files
(
settings
.
gitlab_url
,
settings
.
gitlab_token
,
repo
.
gitlab_project_id
,
ref
=
repo
.
default_
branch
,
tree
=
tree
,
user_query
=
user_query
,
previous_files
=
prev
)
loaded
=
set
()
loaded
=
set
()
for
f
in
result
[
"priority_files"
]:
loaded
.
add
(
f
[
"path"
])
for
f
in
result
[
"priority_files"
]:
loaded
.
add
(
f
[
"path"
])
for
f
in
result
[
"query_files"
]:
loaded
.
add
(
f
[
"path"
])
for
f
in
result
[
"query_files"
]:
loaded
.
add
(
f
[
"path"
])
if
chat
.
id
not
in
_chat_file_history
:
_chat_file_history
[
chat
.
id
]
=
set
()
if
chat
.
id
not
in
_chat_file_history
:
_chat_file_history
[
chat
.
id
]
=
set
()
_chat_file_history
[
chat
.
id
]
.
update
(
loaded
)
_chat_file_history
[
chat
.
id
]
.
update
(
loaded
)
return
self
.
_format_smart_context
(
result
,
tree
,
repo
,
db
,
branch
)
return
self
.
_format_smart_context
(
result
,
tree
,
repo
,
db
)
except
Exception
as
e
:
except
Exception
as
e
:
return
f
"[Repository: {repo.name} — error: {str(e)[:200]}]"
return
f
"[Repository: {repo.name} — error: {str(e)[:200]}]"
def
_format_smart_context
(
self
,
result
,
tree
,
repo
,
db
,
branch
=
None
):
def
_format_smart_context
(
self
,
result
,
tree
,
repo
,
db
):
files_in_tree
=
sorted
([
i
[
"path"
]
for
i
in
tree
if
i
[
"type"
]
==
"blob"
])
files_in_tree
=
sorted
([
i
[
"path"
]
for
i
in
tree
if
i
[
"type"
]
==
"blob"
])
effective_branch
=
branch
or
repo
.
default_branch
lines
=
[
f
"Repository: {repo.name}"
,
f
"Branch: {repo.default_branch}"
,
f
"Files loaded: {result['files_loaded']}"
,
f
"Characters: {result['total_characters']:,}"
]
lines
=
[
f
"Repository: {repo.name}"
,
f
"Branch: {effective_branch}"
,
f
"Files loaded: {result['files_loaded']}"
,
f
"Characters: {result['total_characters']:,}"
]
if
repo
.
architecture_map
and
repo
.
map_status
==
"ready"
:
if
repo
.
architecture_map
and
repo
.
map_status
==
"ready"
:
lines
.
append
(
""
);
lines
.
append
(
repo
.
architecture_map
);
lines
.
append
(
""
)
lines
.
append
(
""
);
lines
.
append
(
repo
.
architecture_map
);
lines
.
append
(
""
)
lines
.
append
(
"═"
*
50
);
lines
.
append
(
"FILE TREE:"
);
lines
.
append
(
"═"
*
50
)
lines
.
append
(
"═"
*
50
);
lines
.
append
(
"FILE TREE:"
);
lines
.
append
(
"═"
*
50
)
...
...
frontend/package.json
View file @
3c51ab07
{
{
"scripts"
:
{
"dev"
:
"vite"
,
"build"
:
"vite build"
,
"preview"
:
"vite preview"
},
"dependencies"
:
{
"dependencies"
:
{
"lucide-react"
:
"^0.469.0"
,
"lucide-react"
:
"^0.469.0"
,
"react"
:
"^18.3.1"
,
"react"
:
"^18.3.1"
,
...
...
frontend/src/api.js
View file @
3c51ab07
This diff is collapsed.
Click to expand it.
frontend/src/components/ChatView.jsx
View file @
3c51ab07
This diff is collapsed.
Click to expand it.
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment