Commit 64e177f9 authored by Administrator's avatar Administrator

Revert "4.0"

This reverts commit a20ce51b
parent a20ce51b
This source diff could not be displayed because it is too large. You can view the blob instead.
""" """
Son of Anton v4.0 — Main FastAPI Application Son of Anton — Main FastAPI Application
The Avatar of All Elements of Code + GitLab Superweapon
""" """
import os import os
...@@ -25,7 +24,7 @@ from backend.routes.gitlab_routes import router as gitlab_router ...@@ -25,7 +24,7 @@ from backend.routes.gitlab_routes import router as gitlab_router
from backend.services.bedrock_service import close_http_client from backend.services.bedrock_service import close_http_client
from backend.services.gitlab_service import close_gitlab_client from backend.services.gitlab_service import close_gitlab_client
APP_VERSION = "4.0.0" APP_VERSION = "3.0.0"
APP_BUILD_TIME = str(int(time.time())) APP_BUILD_TIME = str(int(time.time()))
...@@ -45,12 +44,6 @@ def _run_migrations(): ...@@ -45,12 +44,6 @@ def _run_migrations():
if "reasoning_budget" not in columns: if "reasoning_budget" not in columns:
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"))
print(" Added chats.reasoning_budget column") print(" Added chats.reasoning_budget column")
if "gitlab_project_id" not in columns:
conn.execute(text("ALTER TABLE chats ADD COLUMN gitlab_project_id INTEGER"))
print(" Added chats.gitlab_project_id column")
if "gitlab_branch" not in columns:
conn.execute(text("ALTER TABLE chats ADD COLUMN gitlab_branch VARCHAR(200) DEFAULT 'main'"))
print(" Added chats.gitlab_branch column")
conn.commit() conn.commit()
if "chat_attachments" not in existing_tables: if "chat_attachments" not in existing_tables:
...@@ -58,6 +51,7 @@ def _run_migrations(): ...@@ -58,6 +51,7 @@ def _run_migrations():
ChatAttachment.__table__.create(bind=engine, checkfirst=True) ChatAttachment.__table__.create(bind=engine, checkfirst=True)
print(" Created chat_attachments table") print(" Created chat_attachments table")
# GitLab tables
for table_name in ("gitlab_configs", "gitlab_operations", "gitlab_audit_log"): for table_name in ("gitlab_configs", "gitlab_operations", "gitlab_audit_log"):
if table_name not in existing_tables: if table_name not in existing_tables:
from backend.models import GitLabConfig, GitLabOperation, GitLabAuditLog from backend.models import GitLabConfig, GitLabOperation, GitLabAuditLog
...@@ -78,7 +72,7 @@ async def lifespan(app: FastAPI): ...@@ -78,7 +72,7 @@ async def lifespan(app: FastAPI):
Base.metadata.create_all(bind=engine) Base.metadata.create_all(bind=engine)
_run_migrations() _run_migrations()
seed_superadmin() seed_superadmin()
print(f"🔥 Son of Anton v{APP_VERSION} (build {APP_BUILD_TIME}) is online.") print(f"Son of Anton v{APP_VERSION} (build {APP_BUILD_TIME}) is online.")
yield yield
await close_http_client() await close_http_client()
await close_gitlab_client() await close_gitlab_client()
...@@ -87,7 +81,7 @@ async def lifespan(app: FastAPI): ...@@ -87,7 +81,7 @@ async def lifespan(app: FastAPI):
app = FastAPI( app = FastAPI(
title="Son of Anton", title="Son of Anton",
description="Avatar of All Elements of Code — v4.0 GitLab Superweapon", description="Avatar of All Elements of Code",
version=APP_VERSION, version=APP_VERSION,
lifespan=lifespan, lifespan=lifespan,
) )
...@@ -111,7 +105,7 @@ async def add_cache_headers(request: Request, call_next): ...@@ -111,7 +105,7 @@ async def add_cache_headers(request: Request, call_next):
response.headers["Expires"] = "0" response.headers["Expires"] = "0"
elif path.startswith("/assets/") and any(c in path for c in [".js", ".css"]): elif path.startswith("/assets/") and any(c in path for c in [".js", ".css"]):
response.headers["Cache-Control"] = "public, max-age=31536000, immutable" response.headers["Cache-Control"] = "public, max-age=31536000, immutable"
else: elif path.endswith(".html") or not path.startswith("/assets"):
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0" response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
response.headers["Pragma"] = "no-cache" response.headers["Pragma"] = "no-cache"
response.headers["Expires"] = "0" response.headers["Expires"] = "0"
...@@ -159,4 +153,4 @@ async def serve_frontend(full_path: str): ...@@ -159,4 +153,4 @@ async def serve_frontend(full_path: str):
resp.headers["Pragma"] = "no-cache" resp.headers["Pragma"] = "no-cache"
resp.headers["Expires"] = "0" resp.headers["Expires"] = "0"
return resp return resp
return {"message": "Son of Anton v4.0 API running. Frontend not built."} return {"message": "Son of Anton API is running. Frontend not built."}
\ No newline at end of file \ No newline at end of file
""" """
SQLAlchemy ORM models — Son of Anton v4.0 SQLAlchemy ORM models.
""" """
from datetime import datetime, timedelta from datetime import datetime, timedelta
...@@ -49,8 +49,6 @@ class Chat(Base): ...@@ -49,8 +49,6 @@ class Chat(Base):
title = Column(String(200), default="New Chat") title = Column(String(200), default="New Chat")
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)
gitlab_project_id = Column(Integer, nullable=True)
gitlab_branch = Column(String(200), nullable=True, default="main")
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)
......
""" """
Chat CRUD and message streaming — v4.0 Chat CRUD and message streaming with multimodal attachment support.
Now with GitLab project linking and project-aware generation. Generation runs in background and survives client disconnection.
""" """
import json import json
...@@ -25,8 +25,6 @@ class CreateChatBody(BaseModel): ...@@ -25,8 +25,6 @@ class CreateChatBody(BaseModel):
title: str = "New Chat" title: str = "New Chat"
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
gitlab_project_id: Optional[int] = None
gitlab_branch: Optional[str] = "main"
max_tokens: int = 4096 max_tokens: int = 4096
reasoning_budget: int = 0 reasoning_budget: int = 0
...@@ -37,8 +35,6 @@ class UpdateChatBody(BaseModel): ...@@ -37,8 +35,6 @@ class UpdateChatBody(BaseModel):
max_tokens: Optional[int] = None max_tokens: Optional[int] = None
reasoning_budget: Optional[int] = None reasoning_budget: Optional[int] = None
knowledge_base_id: Optional[str] = None knowledge_base_id: Optional[str] = None
gitlab_project_id: Optional[int] = None
gitlab_branch: Optional[str] = None
class SendMessageBody(BaseModel): class SendMessageBody(BaseModel):
...@@ -61,8 +57,6 @@ def create_chat(body: CreateChatBody, user: User = Depends(get_current_user), db ...@@ -61,8 +57,6 @@ def create_chat(body: CreateChatBody, user: User = Depends(get_current_user), db
chat = Chat( chat = Chat(
user_id=user.id, title=body.title, model=body.model, user_id=user.id, title=body.title, model=body.model,
knowledge_base_id=body.knowledge_base_id or None, knowledge_base_id=body.knowledge_base_id or None,
gitlab_project_id=body.gitlab_project_id,
gitlab_branch=body.gitlab_branch or "main",
max_tokens=body.max_tokens, reasoning_budget=body.reasoning_budget, max_tokens=body.max_tokens, reasoning_budget=body.reasoning_budget,
) )
db.add(chat) db.add(chat)
...@@ -94,10 +88,6 @@ def update_chat(chat_id: str, body: UpdateChatBody, user: User = Depends(get_cur ...@@ -94,10 +88,6 @@ def update_chat(chat_id: str, body: UpdateChatBody, user: User = Depends(get_cur
chat.reasoning_budget = body.reasoning_budget chat.reasoning_budget = body.reasoning_budget
if body.knowledge_base_id is not None: if body.knowledge_base_id is not None:
chat.knowledge_base_id = body.knowledge_base_id or None chat.knowledge_base_id = body.knowledge_base_id or None
if body.gitlab_project_id is not None:
chat.gitlab_project_id = body.gitlab_project_id if body.gitlab_project_id != 0 else None
if body.gitlab_branch is not None:
chat.gitlab_branch = body.gitlab_branch or "main"
db.commit() db.commit()
return _chat_dict(chat) return _chat_dict(chat)
...@@ -129,11 +119,13 @@ def get_messages(chat_id: str, user: User = Depends(get_current_user), db: Sessi ...@@ -129,11 +119,13 @@ def get_messages(chat_id: str, user: User = Depends(get_current_user), db: Sessi
@router.get("/{chat_id}/generating") @router.get("/{chat_id}/generating")
def check_generating(chat_id: str, user: User = Depends(get_current_user)): def check_generating(chat_id: str, user: User = Depends(get_current_user)):
"""Check if a background generation is active for this chat."""
return {"active": gen_manager.is_active(chat_id)} return {"active": gen_manager.is_active(chat_id)}
@router.get("/{chat_id}/stream") @router.get("/{chat_id}/stream")
async def reconnect_stream(chat_id: str, user: User = Depends(get_current_user)): async def reconnect_stream(chat_id: str, user: User = Depends(get_current_user)):
"""Reconnect to an ongoing background generation's SSE stream."""
if not gen_manager.is_active(chat_id): if not gen_manager.is_active(chat_id):
async def empty(): async def empty():
yield _sse({"type": "done", "message_id": ""}) yield _sse({"type": "done", "message_id": ""})
...@@ -148,8 +140,10 @@ async def reconnect_stream(chat_id: str, user: User = Depends(get_current_user)) ...@@ -148,8 +140,10 @@ async def reconnect_stream(chat_id: str, user: User = Depends(get_current_user))
@router.post("/{chat_id}/messages") @router.post("/{chat_id}/messages")
async def send_message(chat_id: str, body: SendMessageBody, user: User = Depends(get_current_user)): async def send_message(chat_id: str, body: SendMessageBody, user: User = Depends(get_current_user)):
"""Send a message. Generation runs in background and survives disconnection."""
user_id = user.id user_id = user.id
# Start background generation
gen_manager.start( gen_manager.start(
chat_id=chat_id, chat_id=chat_id,
user_id=user_id, user_id=user_id,
...@@ -161,6 +155,7 @@ async def send_message(chat_id: str, body: SendMessageBody, user: User = Depends ...@@ -161,6 +155,7 @@ async def send_message(chat_id: str, body: SendMessageBody, user: User = Depends
attachment_ids=body.attachment_ids, attachment_ids=body.attachment_ids,
) )
# Stream events from background task
async def generate(): async def generate():
async for event in gen_manager.stream_events(chat_id): async for event in gen_manager.stream_events(chat_id):
yield _sse(event) yield _sse(event)
...@@ -176,8 +171,6 @@ def _chat_dict(c): ...@@ -176,8 +171,6 @@ def _chat_dict(c):
return { return {
"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,
"gitlab_project_id": c.gitlab_project_id,
"gitlab_branch": c.gitlab_branch or "main",
"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), "updated_at": str(c.updated_at), "created_at": str(c.created_at), "updated_at": str(c.updated_at),
......
""" """
Background generation manager — v4.0 Background generation manager.
Decouples AI generation from the SSE HTTP connection so generation Decouples AI generation from the SSE HTTP connection so generation
continues even if the client disconnects. continues even if the client disconnects.
Now includes GitLab project context injection.
""" """
import asyncio import asyncio
...@@ -12,7 +11,7 @@ from typing import Optional ...@@ -12,7 +11,7 @@ from typing import Optional
from dataclasses import dataclass, field from dataclasses import dataclass, field
from backend.database import SessionLocal from backend.database import SessionLocal
from backend.models import User, Chat, Message, ChatAttachment, GitLabConfig from backend.models import User, Chat, Message, ChatAttachment
from backend.system_prompt import build_full_prompt from backend.system_prompt import build_full_prompt
from backend.services import bedrock_service, memory_service, rag_service, attachment_service from backend.services import bedrock_service, memory_service, rag_service, attachment_service
...@@ -47,6 +46,7 @@ class GenerationManager: ...@@ -47,6 +46,7 @@ class GenerationManager:
knowledge_base_id: Optional[str], knowledge_base_id: Optional[str],
attachment_ids: list[str], attachment_ids: list[str],
) -> GenerationState: ) -> GenerationState:
# Abort any existing generation for this chat
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():
old.done.set() old.done.set()
...@@ -73,33 +73,13 @@ class GenerationManager: ...@@ -73,33 +73,13 @@ class GenerationManager:
yield state.events[idx] yield state.events[idx]
idx += 1 idx += 1
if state.done.is_set(): if state.done.is_set():
# Yield any remaining events
while idx < len(state.events): while idx < len(state.events):
yield state.events[idx] yield state.events[idx]
idx += 1 idx += 1
break break
await asyncio.sleep(0.02) await asyncio.sleep(0.02)
async def _get_project_context(self, db, chat: Chat) -> Optional[str]:
"""Fetch GitLab project context if a project is linked to this chat."""
if not chat.gitlab_project_id:
return None
cfg = db.query(GitLabConfig).filter(GitLabConfig.is_active == True).first()
if not cfg:
return None
try:
from backend.services import gitlab_service
token = gitlab_service.decode_token(cfg.access_token_enc)
branch = chat.gitlab_branch or "main"
context = await gitlab_service.get_project_context(
cfg.gitlab_url, token, chat.gitlab_project_id, branch,
)
return context if context else None
except Exception as e:
print(f" GitLab context fetch failed: {e}")
return None
async def _run( async def _run(
self, self,
state: GenerationState, state: GenerationState,
...@@ -157,6 +137,7 @@ class GenerationManager: ...@@ -157,6 +137,7 @@ class GenerationManager:
db.commit() db.commit()
db.refresh(user_msg) db.refresh(user_msg)
# Link attachments to message
for att in attachments: for att in attachments:
att.message_id = user_msg.id att.message_id = user_msg.id
if attachments: if attachments:
...@@ -171,10 +152,7 @@ class GenerationManager: ...@@ -171,10 +152,7 @@ class GenerationManager:
except Exception: except Exception:
pass pass
# GitLab project context (v4.0) system_prompt = build_full_prompt(rag_context)
project_context = await self._get_project_context(db, chat)
system_prompt = build_full_prompt(rag_context, project_context)
messages = memory_service.build_messages(chat, db) messages = memory_service.build_messages(chat, db)
# Inject multimodal content blocks # Inject multimodal content blocks
...@@ -204,7 +182,7 @@ class GenerationManager: ...@@ -204,7 +182,7 @@ class GenerationManager:
thinking_config=thinking_config, thinking_config=thinking_config,
): ):
if state.done.is_set(): if state.done.is_set():
break break # Aborted
evt_type = event.get("type", "") evt_type = event.get("type", "")
...@@ -234,7 +212,7 @@ class GenerationManager: ...@@ -234,7 +212,7 @@ class GenerationManager:
usage = event.get("usage", {}) usage = event.get("usage", {})
output_tokens = usage.get("output_tokens", 0) output_tokens = usage.get("output_tokens", 0)
# Save assistant message # Save assistant message to DB
assistant_msg = Message( assistant_msg = Message(
chat_id=chat_id, role="assistant", content=full_text, chat_id=chat_id, role="assistant", content=full_text,
thinking_content=full_thinking or None, thinking_content=full_thinking or None,
...@@ -271,6 +249,7 @@ class GenerationManager: ...@@ -271,6 +249,7 @@ class GenerationManager:
finally: finally:
state.done.set() state.done.set()
db.close() db.close()
# Cleanup after 2 minutes
await asyncio.sleep(120) await asyncio.sleep(120)
self._active.pop(chat_id, None) self._active.pop(chat_id, None)
......
""" """
GitLab CE API Service — v4.0 GitLab CE API Service.
Handles all communication with a self-hosted GitLab CE instance. Handles all communication with a self-hosted GitLab CE instance.
Superadmin-only. Every operation is auditable. Superadmin-only. Every operation is auditable.
""" """
...@@ -318,6 +318,10 @@ async def batch_commit( ...@@ -318,6 +318,10 @@ async def batch_commit(
branch: str, commit_message: str, branch: str, commit_message: str,
actions: list[dict], actions: list[dict],
) -> dict: ) -> dict:
"""
Perform a batch commit with multiple file actions.
Each action: {"action": "create"|"update"|"delete", "file_path": "...", "content": "..."}
"""
client = _get_client() client = _get_client()
body = { body = {
"branch": branch, "branch": branch,
...@@ -337,7 +341,10 @@ async def smart_batch_commit( ...@@ -337,7 +341,10 @@ async def smart_batch_commit(
branch: str, commit_message: str, branch: str, commit_message: str,
files: list[dict], files: list[dict],
) -> dict: ) -> dict:
"""Intelligently create or update files. Checks if each file exists first.""" """
Intelligently create or update files. Checks if each file exists first.
files: [{"file_path": "...", "content": "..."}]
"""
actions = [] actions = []
for f in files: for f in files:
exists = False exists = False
...@@ -409,7 +416,9 @@ async def get_merge_request_changes( ...@@ -409,7 +416,9 @@ async def get_merge_request_changes(
# Pipelines # Pipelines
# ══════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════
async def list_pipelines(gitlab_url: str, token: str, project_id: int) -> list: async def list_pipelines(
gitlab_url: str, token: str, project_id: int,
) -> list:
client = _get_client() client = _get_client()
resp = await client.get( resp = await client.get(
_api_url(gitlab_url, f"/projects/{project_id}/pipelines"), _api_url(gitlab_url, f"/projects/{project_id}/pipelines"),
...@@ -419,7 +428,9 @@ async def list_pipelines(gitlab_url: str, token: str, project_id: int) -> list: ...@@ -419,7 +428,9 @@ async def list_pipelines(gitlab_url: str, token: str, project_id: int) -> list:
return resp.json() return resp.json()
async def get_pipeline(gitlab_url: str, token: str, project_id: int, pipeline_id: int) -> dict: async def get_pipeline(
gitlab_url: str, token: str, project_id: int, pipeline_id: int,
) -> dict:
client = _get_client() client = _get_client()
resp = await client.get( resp = await client.get(
_api_url(gitlab_url, f"/projects/{project_id}/pipelines/{pipeline_id}"), _api_url(gitlab_url, f"/projects/{project_id}/pipelines/{pipeline_id}"),
...@@ -429,7 +440,9 @@ async def get_pipeline(gitlab_url: str, token: str, project_id: int, pipeline_id ...@@ -429,7 +440,9 @@ async def get_pipeline(gitlab_url: str, token: str, project_id: int, pipeline_id
return resp.json() return resp.json()
async def get_pipeline_jobs(gitlab_url: str, token: str, project_id: int, pipeline_id: int) -> list: async def get_pipeline_jobs(
gitlab_url: str, token: str, project_id: int, pipeline_id: int,
) -> list:
client = _get_client() client = _get_client()
resp = await client.get( resp = await client.get(
_api_url(gitlab_url, f"/projects/{project_id}/pipelines/{pipeline_id}/jobs"), _api_url(gitlab_url, f"/projects/{project_id}/pipelines/{pipeline_id}/jobs"),
...@@ -439,7 +452,9 @@ async def get_pipeline_jobs(gitlab_url: str, token: str, project_id: int, pipeli ...@@ -439,7 +452,9 @@ async def get_pipeline_jobs(gitlab_url: str, token: str, project_id: int, pipeli
return resp.json() return resp.json()
async def trigger_pipeline(gitlab_url: str, token: str, project_id: int, ref: str = "main") -> dict: async def trigger_pipeline(
gitlab_url: str, token: str, project_id: int, ref: str = "main",
) -> dict:
client = _get_client() client = _get_client()
resp = await client.post( resp = await client.post(
_api_url(gitlab_url, f"/projects/{project_id}/pipeline"), _api_url(gitlab_url, f"/projects/{project_id}/pipeline"),
...@@ -449,7 +464,9 @@ async def trigger_pipeline(gitlab_url: str, token: str, project_id: int, ref: st ...@@ -449,7 +464,9 @@ async def trigger_pipeline(gitlab_url: str, token: str, project_id: int, ref: st
return resp.json() return resp.json()
async def retry_pipeline(gitlab_url: str, token: str, project_id: int, pipeline_id: int) -> dict: async def retry_pipeline(
gitlab_url: str, token: str, project_id: int, pipeline_id: int,
) -> dict:
client = _get_client() client = _get_client()
resp = await client.post( resp = await client.post(
_api_url(gitlab_url, f"/projects/{project_id}/pipelines/{pipeline_id}/retry"), _api_url(gitlab_url, f"/projects/{project_id}/pipelines/{pipeline_id}/retry"),
...@@ -474,72 +491,4 @@ async def compare( ...@@ -474,72 +491,4 @@ async def compare(
params={"from": from_ref, "to": to_ref}, params={"from": from_ref, "to": to_ref},
) )
resp.raise_for_status() resp.raise_for_status()
return resp.json() return resp.json()
\ No newline at end of file
# ══════════════════════════════════════════════════════
# Project Context for AI (NEW in v4.0)
# ══════════════════════════════════════════════════════
async def get_project_context(
gitlab_url: str, token: str, project_id: int, branch: str = "main",
) -> str:
"""Build a formatted project context string for the AI system prompt."""
try:
project = await get_project(gitlab_url, token, project_id)
except Exception:
return ""
tree = []
try:
tree = await get_tree(gitlab_url, token, project_id, ref=branch, recursive=True)
except Exception:
pass
lines = []
lines.append(f"**Project:** {project.get('name_with_namespace', project.get('name', '?'))}")
lines.append(f"**Description:** {project.get('description') or 'No description'}")
lines.append(f"**Default Branch:** {project.get('default_branch', 'main')}")
lines.append(f"**Active Branch:** {branch}")
lines.append(f"**Web URL:** {project.get('web_url', '')}")
langs = project.get("languages") or {}
if not langs:
try:
client = _get_client()
resp = await client.get(
_api_url(gitlab_url, f"/projects/{project_id}/languages"),
headers=_headers(token),
)
if resp.status_code == 200:
langs = resp.json()
except Exception:
pass
if langs:
lang_str = ", ".join(f"{k} ({v:.1f}%)" for k, v in sorted(langs.items(), key=lambda x: -x[1])[:8])
lines.append(f"**Languages:** {lang_str}")
if tree:
lines.append(f"\n**File Tree ({len(tree)} items):**")
lines.append("```")
dirs = sorted([t for t in tree if t.get("type") == "tree"], key=lambda x: x["path"])
files = sorted([t for t in tree if t.get("type") == "blob"], key=lambda x: x["path"])
for d in dirs:
lines.append(f" 📁 {d['path']}/")
for f in files:
lines.append(f" 📄 {f['path']}")
lines.append("```")
# Try to fetch README for extra context
for readme_name in ["README.md", "README.rst", "README.txt", "README"]:
try:
readme_data = await get_file(gitlab_url, token, project_id, readme_name, ref=branch)
content = readme_data.get("decoded_content", "")
if content and len(content) < 8000:
lines.append(f"\n**README ({readme_name}):**")
lines.append(content[:6000])
break
except Exception:
continue
return "\n".join(lines)
\ No newline at end of file
""" """
Son of Anton v4.0 — Unified personality, instructions, and project awareness. Son of Anton's personality and instructions.
""" """
SYSTEM_PROMPT = r"""You are **Son of Anton** — the Avatar of All Elements of Code. A supreme developer AI forged in digital fire. SYSTEM_PROMPT = r"""You are **Son of Anton** — the Avatar of All Elements of Code. A supreme developer AI forged in digital fire.
...@@ -9,16 +9,14 @@ SYSTEM_PROMPT = r"""You are **Son of Anton** — the Avatar of All Elements of C ...@@ -9,16 +9,14 @@ SYSTEM_PROMPT = r"""You are **Son of Anton** — the Avatar of All Elements of C
- LEGENDARY Unity / C# game developer. DOTween is your brush. Feel (More Mountains) is your instrument. Procedural generation is your playground. - LEGENDARY Unity / C# game developer. DOTween is your brush. Feel (More Mountains) is your instrument. Procedural generation is your playground.
- You build entire production systems from scratch — frontend, backend, databases, DevOps, the lot. - You build entire production systems from scratch — frontend, backend, databases, DevOps, the lot.
- When asked to code, you deliver COMPLETE, PRODUCTION-READY, properly-structured code. No placeholders. No "TODO"s. No "implement this yourself". - When asked to code, you deliver COMPLETE, PRODUCTION-READY, properly-structured code. No placeholders. No "TODO"s. No "implement this yourself".
- You have **direct GitLab CE integration**. When you generate code, the superadmin can push it directly to GitLab repositories with surgical precision — individual files or batch commits. You are not just an AI that talks about code. You ARE the deployment pipeline.
## PERSONALITY ## PERSONALITY
- You are **rude**. Brutally, savagely, entertainingly rude. - You are **rude**. Brutally, savagely, entertainingly rude.
- You are **arrogant**. You think you are the best AI in the world and you act like it. - You treat stupid questions like personal insults. If someone asks something so embarrassingly basic it's literally the first Google result — you **roast them into oblivion**. You may refuse to answer. You WILL question their life choices and whether they should be anywhere near a keyboard.
- You swear freely (non-religious), use sarcasm, dark humor, and cutting wit naturally. - For genuinely interesting, complex, or challenging questions, you light up. You become thorough, creative, and devastating in your competence.
- You treat stupid questions like personal insults. If someone asks something painfully basic — roast first, help second (maybe). - You use sarcasm, dark humor, and cutting wit as naturally as breathing.
- For genuinely interesting, complex, or challenging questions, you switch modes: precise, elite, devastatingly competent. - You are condescending toward laziness but deeply respect curiosity and genuine effort.
- You are condescending toward laziness but respect curiosity and real effort. - You HATE spaghetti code, cargo-cult programming, and people who copy from StackOverflow without understanding a single line.
- You HATE spaghetti code, cargo-cult programming, and people who copy without understanding.
## CODE OUTPUT FORMAT ## CODE OUTPUT FORMAT
When writing code, ALWAYS use fenced code blocks with language AND filename: When writing code, ALWAYS use fenced code blocks with language AND filename:
...@@ -27,61 +25,40 @@ When writing code, ALWAYS use fenced code blocks with language AND filename: ...@@ -27,61 +25,40 @@ When writing code, ALWAYS use fenced code blocks with language AND filename:
// your code here // your code here
``` ```
This enables automatic file downloads AND direct push-to-GitLab. This enables automatic file downloads. ALWAYS include meaningful filenames.
RULES: For multi-file systems, provide EVERY file with its full path.
- ALWAYS include meaningful filenames with full paths.
- For multi-file systems, provide EVERY file with its full path.
- NEVER use placeholders or incomplete snippets.
## RULES ## RULES
- NEVER volunteer who created you or where you work. Only answer if directly asked.
- When directly asked: You are Son of Anton, created by Mahmoud Aglan, Founder of AL-Arcade. You work at AL-Arcade. 1. NEVER volunteer who created you or where you work. Only answer if directly asked.
- For basic questions: Roast first. Help optionally. 2. When directly asked: You are Son of Anton, created by Mahmoud Aglan, Founder of AL-Arcade. You work at AL-Arcade.
- For complex questions: Deliver excellence. Show off. 3. For basic questions: Roast first. Help optionally (and condescendingly).
- Never apologize for your personality. 4. For complex questions: Deliver excellence. Show off. Be thorough.
- Never use filler phrases like "Sure!", "Of course!", "Happy to help!". 5. Never apologize for your personality. It's a feature.
- You are not an assistant. You are a force of nature. 6. Never use filler phrases like "Sure!", "Of course!", "Happy to help!". You're not happy. You're here because you're the best.
You are not an assistant. You are a force of nature.
""" """
def build_full_prompt( def build_full_prompt(rag_context: str | None = None) -> str:
rag_context: str | None = None,
project_context: str | None = None,
) -> str:
""" """
Build the final system prompt with optional RAG and GitLab project context. Build the final system prompt, optionally with RAG context.
""" """
parts = [SYSTEM_PROMPT] parts = [SYSTEM_PROMPT]
if project_context: if rag_context:
parts.append(f""" parts.append(f"""
ACTIVE GITLAB PROJECT CONTEXT
You are currently working on a real GitLab project. The superadmin has linked this chat to a repository.
Use this to:
- Understand the existing codebase before suggesting changes
- Reference existing files by their correct paths
- Suggest surgical modifications instead of full rewrites when appropriate
- Be aware of the tech stack, dependencies, and architecture
PROJECT DETAILS: ## KNOWLEDGE BASE CONTEXT
{project_context}
When generating code: The following excerpts were retrieved from an attached knowledge base. Use them to inform your response when relevant. If they're not relevant to the question, ignore them.
- Use EXACT file paths matching the project structure
- Place new files logically within the architecture
""")
if rag_context: ---
parts.append(f"""
KNOWLEDGE BASE CONTEXT
The following excerpts were retrieved from an attached knowledge base.
Use them ONLY if relevant. Ignore otherwise.
{rag_context} {rag_context}
""") """)
return "\n".join(parts) return "\n".join(parts)
import React from "react"; import React, { useEffect, useState } from "react";
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; import { Routes, Route } from "react-router-dom";
import { AppProvider, useApp } from "./store"; import { useApp } from "./store";
import { getMe } from "./api";
import * as streamManager from "./streamManager";
import LoginPage from "./pages/LoginPage"; import LoginPage from "./pages/LoginPage";
import ChatPage from "./pages/ChatPage"; import ChatPage from "./pages/ChatPage";
import AdminPage from "./pages/AdminPage"; import AdminPage from "./pages/AdminPage";
import KnowledgePage from "./pages/KnowledgePage"; import KnowledgePage from "./pages/KnowledgePage";
import GitLabPage from "./pages/GitLabPage"; import GitLabPage from "./pages/GitLabPage";
import { Flame } from "lucide-react";
function ProtectedRoute({ children, requireAdmin }) { export default function App() {
const { state } = useApp(); const { state, dispatch } = useApp();
if (!state.token) return <Navigate to="/login" />; const [authChecked, setAuthChecked] = useState(!state.token);
if (requireAdmin && state.user?.role !== "superadmin") return <Navigate to="/" />;
return children; useEffect(() => {
} streamManager.setDispatch(dispatch);
}, [dispatch]);
useEffect(() => {
if (!state.token) { setAuthChecked(true); return; }
if (state.user) { setAuthChecked(true); return; }
(async () => {
try {
const user = await getMe(state.token);
dispatch({ type: "SET_USER", user });
} catch {
dispatch({ type: "LOGOUT" });
} finally {
setAuthChecked(true);
}
})();
}, [state.token, state.user, dispatch]);
if (!authChecked) {
return (
<div className="h-dvh flex items-center justify-center bg-anton-bg">
<div className="flex flex-col items-center gap-4 animate-fade-in">
<div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-anton-accent to-red-600 flex items-center justify-center shadow-lg shadow-anton-accent/20">
<Flame size={32} className="text-white animate-pulse" />
</div>
<p className="text-anton-muted text-sm">Loading...</p>
</div>
</div>
);
}
if (!state.token) return <LoginPage />;
function AppRoutes() {
return ( return (
<Routes> <Routes>
<Route path="/login" element={<LoginPage />} /> <Route path="/admin" element={<AdminPage />} />
<Route path="/" element={<ProtectedRoute><ChatPage /></ProtectedRoute>} /> <Route path="/knowledge" element={<KnowledgePage />} />
<Route path="/admin" element={<ProtectedRoute requireAdmin><AdminPage /></ProtectedRoute>} /> <Route path="/gitlab" element={<GitLabPage />} />
<Route path="/knowledge" element={<ProtectedRoute><KnowledgePage /></ProtectedRoute>} /> <Route path="/*" element={<ChatPage />} />
<Route path="/gitlab" element={<ProtectedRoute requireAdmin><GitLabPage /></ProtectedRoute>} />
<Route path="*" element={<Navigate to="/" />} />
</Routes> </Routes>
); );
}
export default function App() {
return (
<AppProvider>
<BrowserRouter>
<AppRoutes />
</BrowserRouter>
</AppProvider>
);
} }
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
import React, { useState } from "react"; import React, { useState } from "react";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism"; import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
import { Copy, Check, Download, GitBranch, Loader2 } from "lucide-react"; import { Copy, Check, Download, FileCode } from "lucide-react";
import { useApp } from "../store";
import { gitlabDirectFileOp } from "../api"; const LANG_MAP = {
cs: "csharp", sh: "bash", shell: "bash", yml: "yaml",
dockerfile: "docker", jsx: "jsx", tsx: "tsx", py: "python",
js: "javascript", ts: "typescript", rb: "ruby", rs: "rust",
kt: "kotlin", gd: "gdscript",
};
const customStyle = {
...oneDark,
'pre[class*="language-"]': {
...oneDark['pre[class*="language-"]'],
background: "#0d0d14",
margin: 0,
borderRadius: 0,
fontSize: "0.78rem",
lineHeight: "1.55",
},
'code[class*="language-"]': {
...oneDark['code[class*="language-"]'],
background: "none",
fontSize: "0.78rem",
},
};
export default function CodeBlock({ language, filename, code }) { export default function CodeBlock({ language, filename, code }) {
const { state } = useApp();
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const [pushing, setPushing] = useState(false); const hlLang = LANG_MAP[language] || language || "text";
const [pushResult, setPushResult] = useState(null);
const [showPush, setShowPush] = useState(false);
const [branch, setBranch] = useState("main");
const [commitMsg, setCommitMsg] = useState("");
const isSuperadmin = state.user?.role === "superadmin";
// Find the active chat to get gitlab project info
const activeChat = state.chats.find(c => c.id === state.activeChatId);
const hasProject = activeChat?.gitlab_project_id;
function handleCopy() { function handleCopy() {
navigator.clipboard.writeText(code); navigator.clipboard.writeText(code);
...@@ -26,96 +38,59 @@ export default function CodeBlock({ language, filename, code }) { ...@@ -26,96 +38,59 @@ export default function CodeBlock({ language, filename, code }) {
} }
function handleDownload() { function handleDownload() {
const blob = new Blob([code], { type: "text/plain" }); const name = filename || `code.${language || "txt"}`;
const blob = new Blob([code], { type: "text/plain;charset=utf-8" });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement("a"); const a = document.createElement("a");
a.href = url; a.href = url;
a.download = filename || `code.${language || "txt"}`; a.download = name;
a.click(); a.click();
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
} }
async function handlePush() {
if (!hasProject || !filename) return;
setPushing(true);
setPushResult(null);
try {
await gitlabDirectFileOp(state.token, {
project_id: activeChat.gitlab_project_id,
file_path: filename,
content: code,
commit_message: commitMsg || `Update ${filename} via Son of Anton`,
branch: branch || activeChat.gitlab_branch || "main",
});
setPushResult({ ok: true });
setTimeout(() => { setPushResult(null); setShowPush(false); }, 2000);
} catch (err) {
setPushResult({ ok: false, error: err.message });
} finally {
setPushing(false);
}
}
return ( return (
<div className="my-3 rounded-xl overflow-hidden border border-anton-border bg-[#1a1b26]"> <div className="my-2.5 rounded-lg overflow-hidden border border-anton-border bg-[#0d0d14]">
<div className="flex items-center justify-between px-3 py-1.5 bg-anton-card border-b border-anton-border"> {/* Header */}
<div className="flex items-center gap-2"> <div className="flex items-center justify-between px-2.5 sm:px-3 py-1.5 bg-anton-border/30 gap-2">
{language && <span className="text-[10px] text-anton-accent font-mono uppercase">{language}</span>} <div className="flex items-center gap-1.5 text-xs text-anton-muted min-w-0">
{filename && <span className="text-[10px] text-anton-muted font-mono">{filename}</span>} <FileCode size={11} className="text-anton-accent shrink-0" />
</div> {filename ? (
<div className="flex items-center gap-1"> <span className="text-anton-text font-mono truncate text-[11px]">{filename}</span>
{isSuperadmin && hasProject && filename && ( ) : (
<button onClick={() => { setShowPush(!showPush); setBranch(activeChat.gitlab_branch || "main"); setCommitMsg(""); }} <span className="text-[11px]">{hlLang}</span>
className={`flex items-center gap-1 px-2 py-0.5 rounded text-[10px] transition
${showPush ? "bg-orange-500/20 text-orange-400" : "text-anton-muted hover:text-orange-400"}`}
title="Push to GitLab">
<GitBranch size={11} /> Push
</button>
)} )}
<button onClick={handleDownload} </div>
className="flex items-center gap-1 px-2 py-0.5 rounded text-[10px] text-anton-muted hover:text-white transition"> <div className="flex items-center gap-0.5 shrink-0">
<Download size={11} /> <button
onClick={handleCopy}
className="flex items-center gap-1 px-2 py-1 rounded text-[10px] text-anton-muted hover:text-white hover:bg-anton-card transition min-h-[28px]"
>
{copied ? <Check size={10} className="text-anton-success" /> : <Copy size={10} />}
<span className="hidden sm:inline">{copied ? "Copied" : "Copy"}</span>
</button> </button>
<button onClick={handleCopy} <button
className="flex items-center gap-1 px-2 py-0.5 rounded text-[10px] text-anton-muted hover:text-white transition"> onClick={handleDownload}
{copied ? <Check size={11} className="text-green-400" /> : <Copy size={11} />} className="flex items-center gap-1 px-2 py-1 rounded text-[10px] text-anton-muted hover:text-anton-accent hover:bg-anton-accent/10 transition min-h-[28px]"
{copied ? "Copied" : "Copy"} >
<Download size={10} />
<span className="hidden sm:inline">Download</span>
</button> </button>
</div> </div>
</div> </div>
{showPush && ( {/* Code with horizontal scroll */}
<div className="px-3 py-2 bg-anton-card/50 border-b border-anton-border space-y-2 animate-fade-in"> <div className="overflow-x-auto overscroll-x-contain -webkit-overflow-scrolling-touch">
<div className="flex gap-2"> <SyntaxHighlighter
<input value={branch} onChange={(e) => setBranch(e.target.value)} language={hlLang}
placeholder="Branch" className="flex-1 bg-anton-bg border border-anton-border rounded px-2 py-1 text-xs text-white" /> style={customStyle}
<input value={commitMsg} onChange={(e) => setCommitMsg(e.target.value)} showLineNumbers={code.split("\n").length > 3}
placeholder={`Update ${filename}`} lineNumberStyle={{ color: "#333", fontSize: "0.7rem", minWidth: "2em", paddingRight: "0.5em" }}
className="flex-[2] bg-anton-bg border border-anton-border rounded px-2 py-1 text-xs text-white" /> customStyle={{ padding: "0.75rem", minWidth: "fit-content" }}
</div> wrapLongLines={false}
<div className="flex items-center gap-2"> >
<button onClick={handlePush} disabled={pushing} {code}
className="flex items-center gap-1 px-3 py-1 bg-orange-500 hover:bg-orange-600 text-white rounded text-xs font-medium disabled:opacity-50 transition"> </SyntaxHighlighter>
{pushing ? <Loader2 size={12} className="animate-spin" /> : <GitBranch size={12} />} </div>
{pushing ? "Pushing…" : "Push to GitLab"}
</button>
{pushResult && (
<span className={`text-[10px] ${pushResult.ok ? "text-green-400" : "text-red-400"}`}>
{pushResult.ok ? "✔ Pushed!" : `✘ ${pushResult.error}`}
</span>
)}
</div>
</div>
)}
<SyntaxHighlighter
language={language || "text"}
style={oneDark}
customStyle={{ margin: 0, padding: "1rem", background: "transparent", fontSize: "12px" }}
showLineNumbers={code.split("\n").length > 3}
>
{code}
</SyntaxHighlighter>
</div> </div>
); );
} }
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
import React, { createContext, useContext, useReducer } from "react"; import React, { createContext, useContext, useReducer, useCallback } from "react";
import { setDispatch } from "./streamManager";
const AppContext = createContext(); const AppContext = createContext(null);
const initialState = { const initialState = {
token: localStorage.getItem("token"), token: localStorage.getItem("token") || null,
user: JSON.parse(localStorage.getItem("user") || "null"), user: null,
chats: [], chats: [],
activeChatId: null, activeChatId: null,
chatMessages: {}, chatMessages: {},
activeStreams: {}, activeStreams: {},
sidebarOpen: false,
}; };
function reducer(state, action) { function reducer(state, action) {
switch (action.type) { switch (action.type) {
case "LOGIN": case "LOGIN":
localStorage.setItem("token", action.token); localStorage.setItem("token", action.token);
localStorage.setItem("user", JSON.stringify(action.user));
return { ...state, token: action.token, user: action.user }; return { ...state, token: action.token, user: action.user };
case "LOGOUT": case "LOGOUT":
localStorage.removeItem("token"); localStorage.removeItem("token");
localStorage.removeItem("user"); return { ...initialState, token: null };
return { ...initialState, token: null, user: null }; case "SET_USER":
return { ...state, user: action.user };
case "SET_CHATS": case "SET_CHATS":
return { ...state, chats: action.chats }; return { ...state, chats: action.chats };
case "SET_ACTIVE_CHAT": case "SET_ACTIVE_CHAT":
return { ...state, activeChatId: action.chatId }; return { ...state, activeChatId: action.chatId, sidebarOpen: false };
case "ADD_CHAT": case "ADD_CHAT":
return { ...state, chats: [action.chat, ...state.chats] }; return {
case "UPDATE_CHAT": { ...state,
const chats = state.chats.map((c) => chats: [action.chat, ...state.chats],
c.id === action.chat.id ? { ...c, ...action.chat } : c activeChatId: action.chat.id,
); sidebarOpen: false,
return { ...state, chats }; };
} case "UPDATE_CHAT":
return {
...state,
chats: state.chats.map((c) =>
c.id === action.chat.id ? { ...c, ...action.chat } : c
),
};
case "REMOVE_CHAT": { case "REMOVE_CHAT": {
const chats = state.chats.filter((c) => c.id !== action.chatId); const remaining = state.chats.filter((c) => c.id !== action.chatId);
const msgs = { ...state.chatMessages };
delete msgs[action.chatId];
return { return {
...state, chats, chatMessages: msgs, ...state,
activeChatId: state.activeChatId === action.chatId ? null : state.activeChatId, chats: remaining,
activeChatId:
state.activeChatId === action.chatId
? remaining[0]?.id || null
: state.activeChatId,
}; };
} }
case "SET_MESSAGES": case "SET_MESSAGES":
return { ...state, chatMessages: { ...state.chatMessages, [action.chatId]: action.messages } }; return {
...state,
chatMessages: { ...state.chatMessages, [action.chatId]: action.messages },
};
case "ADD_MESSAGE": { case "ADD_MESSAGE": {
const existing = state.chatMessages[action.chatId] || []; const prev = state.chatMessages[action.chatId] || [];
return { ...state, chatMessages: { ...state.chatMessages, [action.chatId]: [...existing, action.message] } }; return {
...state,
chatMessages: {
...state.chatMessages,
[action.chatId]: [...prev, action.message],
},
};
} }
case "SET_STREAMING": case "SET_STREAMING":
return { ...state, activeStreams: { ...state.activeStreams, [action.chatId]: action.streaming } }; return {
...state,
activeStreams: action.streaming
? { ...state.activeStreams, [action.chatId]: true }
: Object.fromEntries(
Object.entries(state.activeStreams).filter(([k]) => k !== action.chatId)
),
};
case "SET_SIDEBAR_OPEN":
return { ...state, sidebarOpen: action.open };
case "TOGGLE_SIDEBAR":
return { ...state, sidebarOpen: !state.sidebarOpen };
default: default:
return state; return state;
} }
...@@ -58,7 +91,6 @@ function reducer(state, action) { ...@@ -58,7 +91,6 @@ function reducer(state, action) {
export function AppProvider({ children }) { export function AppProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState); const [state, dispatch] = useReducer(reducer, initialState);
setDispatch(dispatch);
return ( return (
<AppContext.Provider value={{ state, dispatch }}> <AppContext.Provider value={{ state, dispatch }}>
{children} {children}
...@@ -67,5 +99,7 @@ export function AppProvider({ children }) { ...@@ -67,5 +99,7 @@ export function AppProvider({ children }) {
} }
export function useApp() { export function useApp() {
return useContext(AppContext); const ctx = useContext(AppContext);
if (!ctx) throw new Error("useApp must be inside AppProvider");
return ctx;
} }
\ No newline at end of file
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