Commit 3c51ab07 authored by Mahmoud Aglan's avatar Mahmoud Aglan

lol

parent 1cd1b7d6
......@@ -42,14 +42,13 @@ def _run_migrations():
conn.execute(text("ALTER TABLE chats ADD COLUMN reasoning_budget INTEGER DEFAULT 0"))
if "linked_repo_id" not in columns:
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()
if "chat_attachments" not in existing_tables:
from backend.models import ChatAttachment
ChatAttachment.__table__.create(bind=engine, checkfirst=True)
# Create user_permissions table if missing
if "user_permissions" not in existing_tables:
from backend.models import UserPermissions
UserPermissions.__table__.create(bind=engine, checkfirst=True)
......
......@@ -54,6 +54,7 @@ class UserPermissions(Base):
unique=True, nullable=False, index=True,
)
# Feature access
can_use_web_search = Column(Boolean, default=False)
can_use_ui_design = Column(Boolean, default=False)
can_use_knowledge_base = Column(Boolean, default=True)
......@@ -62,8 +63,10 @@ class UserPermissions(Base):
can_export_pptx = 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")
# Limits (0 = unlimited for count-based limits)
max_tokens_cap = Column(Integer, default=4096)
max_reasoning_budget = Column(Integer, default=0)
max_chats = Column(Integer, default=50)
......@@ -87,7 +90,6 @@ class Chat(Base):
model = Column(String(100), default="eu.anthropic.claude-opus-4-6-v1")
knowledge_base_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)
reasoning_budget = Column(Integer, default=0)
created_at = Column(DateTime, default=datetime.utcnow)
......
......@@ -28,7 +28,6 @@ class CreateChatBody(BaseModel):
model: str = "eu.anthropic.claude-opus-4-6-v1"
knowledge_base_id: Optional[str] = None
linked_repo_id: Optional[str] = None
linked_repo_branch: Optional[str] = None
max_tokens: int = 4096
reasoning_budget: int = 0
......@@ -40,7 +39,6 @@ class UpdateChatBody(BaseModel):
reasoning_budget: Optional[int] = None
knowledge_base_id: Optional[str] = None
linked_repo_id: Optional[str] = None
linked_repo_branch: Optional[str] = None
class SendMessageBody(BaseModel):
......@@ -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),
knowledge_base_id=body.knowledge_base_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)),
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
if body.linked_repo_id and not perms.get("can_use_gitlab"):
raise HTTPException(403, "GitLab not enabled.")
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()
return _chat_dict(chat, db)
......@@ -210,6 +205,11 @@ async def commit_from_chat(
user: User = Depends(get_current_user),
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)
chat = db.query(Chat).filter(Chat.id == chat_id, Chat.user_id == user.id).first()
......@@ -226,44 +226,73 @@ async def commit_from_chat(
if not settings or not settings.is_active:
raise HTTPException(400, "GitLab not configured")
# ── Fetch repo tree to know which files already exist ──
existing_paths = set()
try:
tree = await gitlab_service.get_tree(
settings.gitlab_url, settings.gitlab_token,
repo.gitlab_project_id, ref=body.branch, recursive=True,
settings.gitlab_url,
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:
# If tree fetch fails (empty repo, network issue, etc.),
# we'll try all as "create" since we can't know what exists
pass
# ── Build actions with auto-detected create/update ──
actions = []
for f in body.files:
file_path = f.get("file_path", "")
content = f.get("content", "")
requested_action = f.get("action", "auto")
if not file_path or not content:
continue
file_exists = file_path in existing_paths
# Smart action resolution
if requested_action in ("auto", "upsert"):
# Auto-detect: update if exists, create if not
actual_action = "update" if file_exists else "create"
elif requested_action == "update" and not file_exists:
# User said update but file doesn't exist → create instead
actual_action = "create"
elif requested_action == "create" and file_exists:
# User said create but file already exists → update instead
actual_action = "update"
else:
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:
raise HTTPException(400, "No valid files to commit")
try:
result = await gitlab_service.commit_files(
settings.gitlab_url, settings.gitlab_token,
repo.gitlab_project_id, body.branch, body.commit_message, actions,
settings.gitlab_url,
settings.gitlab_token,
repo.gitlab_project_id,
body.branch,
body.commit_message,
actions,
)
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:
raise HTTPException(e.status_code, f"Commit failed: {e.detail}")
......@@ -285,7 +314,6 @@ def _chat_dict(c, db=None):
"id": c.id, "title": c.title, "model": c.model,
"knowledge_base_id": c.knowledge_base_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,
"reasoning_budget": c.reasoning_budget or 0,
"created_at": str(c.created_at),
......
"""
GitLab CE integration routes — superadmin + user-facing.
GitLab CE integration routes — superadmin only.
Son of Anton v4.2.0
"""
......@@ -13,9 +13,9 @@ from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from backend.database import get_db
from backend.models import User, GitLabSettings, LinkedRepo, PendingAction, KnowledgeBase, KnowledgeDocument
from backend.auth import require_superadmin, get_current_user, check_feature
from backend.services import gitlab_service, code_analyzer, rag_service
from backend.models import User, GitLabSettings, LinkedRepo, PendingAction
from backend.auth import require_superadmin
from backend.services import gitlab_service, code_analyzer
router = APIRouter()
......@@ -64,10 +64,6 @@ class ActionBody(BaseModel):
title: str = ""
payload: str
class CreateKBFromRepoBody(BaseModel):
branch: Optional[str] = None
name: Optional[str] = None
def _get_settings(db: Session) -> GitLabSettings:
s = db.query(GitLabSettings).first()
......@@ -350,10 +346,13 @@ async def create_branch(repo_id: str, body: BranchBody, admin: User = Depends(re
@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)):
"""Commit multiple files. Auto-detects create vs update per file."""
"""
Commit multiple files. Auto-detects create vs update per file.
"""
s = _get_settings(db)
repo = _get_repo(repo_id, db)
# Fetch tree to know which files exist
existing_paths = set()
try:
tree = await gitlab_service.get_tree(
......@@ -369,9 +368,12 @@ async def commit_code(repo_id: str, body: CommitBody, admin: User = Depends(requ
file_path = a.get("file_path", "")
content = a.get("content", "")
requested = a.get("action", "auto")
if not file_path:
continue
file_exists = file_path in existing_paths
if requested in ("auto", "upsert"):
actual = "update" if file_exists else "create"
elif requested == "update" and not file_exists:
......@@ -380,7 +382,12 @@ async def commit_code(repo_id: str, body: CommitBody, admin: User = Depends(requ
actual = "update"
else:
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:
raise HTTPException(400, "No valid files to commit")
......@@ -402,10 +409,13 @@ async def commit_single(
admin: User = Depends(require_superadmin),
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)
repo = _get_repo(repo_id, db)
# Auto-detect whether file exists
action = body.action
if action in ("update", "create", "auto"):
try:
......@@ -537,153 +547,6 @@ def reject_action(action_id: str, admin: User = Depends(require_superadmin), db:
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
# ═══════════════════════════════════════════════════
......
"""
Background generation manager — v4.2.0 with web search + branch-aware repo context.
Background generation manager — v4.1.0 with web search.
"""
import asyncio
......@@ -47,9 +47,6 @@ class GenerationManager:
state = self._active.get(chat_id)
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):
old = self._active.get(chat_id)
if old and not old.done.is_set():
......@@ -82,27 +79,25 @@ class GenerationManager:
if not repo: return None
settings = db.query(GitLabSettings).first()
if not settings or not settings.is_active: return None
branch = getattr(chat, 'linked_repo_branch', None) or repo.default_branch
try:
tree = _get_tree_cache(repo.id, branch)
tree = _get_tree_cache(repo.id, repo.default_branch)
if tree is None:
tree = await gitlab_service.get_tree(settings.gitlab_url, settings.gitlab_token, repo.gitlab_project_id, ref=branch, recursive=True)
_set_tree_cache(repo.id, branch, tree)
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, repo.default_branch, tree)
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()
for f in result["priority_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()
_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:
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"])
effective_branch = branch or repo.default_branch
lines = [f"Repository: {repo.name}", f"Branch: {effective_branch}", f"Files loaded: {result['files_loaded']}", f"Characters: {result['total_characters']:,}"]
lines = [f"Repository: {repo.name}", f"Branch: {repo.default_branch}", f"Files loaded: {result['files_loaded']}", f"Characters: {result['total_characters']:,}"]
if repo.architecture_map and repo.map_status == "ready":
lines.append(""); lines.append(repo.architecture_map); lines.append("")
lines.append("═" * 50); lines.append("FILE TREE:"); lines.append("═" * 50)
......
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"lucide-react": "^0.469.0",
"react": "^18.3.1",
......
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment