MrA7A3 commited on
Commit
4f96544
·
verified ·
1 Parent(s): 2773db8

Upload 35 files

Browse files
Dockerfile ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+ WORKDIR /app
3
+ ENV PYTHONUNBUFFERED=1
4
+ ENV PYTHONUTF8=1
5
+ COPY requirements.txt /app/requirements.txt
6
+ RUN pip install --no-cache-dir -r /app/requirements.txt
7
+ RUN mkdir -p /data/kapo_runtime/current /data/kapo_runtime/overlay
8
+ COPY brain_server /app/brain_server
9
+ COPY bootstrap_space_runtime.py /app/bootstrap_space_runtime.py
10
+ CMD ["python", "/app/bootstrap_space_runtime.py"]
README.md CHANGED
@@ -1,11 +1,21 @@
1
- ---
2
- title: AiDebugger
3
- emoji: 🏢
4
- colorFrom: purple
5
- colorTo: pink
6
- sdk: docker
7
- pinned: false
8
- short_description: AiDebugger
9
- ---
10
-
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: hf_debugger_main
3
+ emoji: 🤖
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: docker
7
+ pinned: false
8
+ ---
9
+
10
+ # hf_debugger_main
11
+
12
+ Generated Hugging Face deployment package from KAPO Control Center.
13
+
14
+ Model profile: hf-debugger-qwen25-7b-instruct
15
+ Model repo: Qwen/Qwen2.5-1.5B-Instruct
16
+ Model file: not set
17
+ Roles: fallback
18
+ Languages: ar, en
19
+
20
+ This is a Docker-oriented Hugging Face Space package.
21
+ Push the extracted contents to a Docker Space repository, configure secrets/env vars, then set the resulting Base URL in the platform registry.
bootstrap_space_runtime.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import shutil
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ DEFAULT_ENV = {
9
+ "REMOTE_BRAIN_ONLY": "1",
10
+ "KAGGLE_AUTO_BOOTSTRAP": "0",
11
+ "BRAIN_AUTO_NGROK": "0",
12
+ "BRAIN_AUTO_PUBLISH_URL_ON_STARTUP": "0",
13
+ "BRAIN_REUSE_PUBLIC_URL_ON_RESTART": "0",
14
+ "HF_SPACE_DOCKER": "1",
15
+ "KAPO_COMPUTE_PROFILE": "cpu",
16
+ "HF_ACCELERATOR": "cpu",
17
+ "KAPO_HF_TRANSFORMERS_RUNTIME": "1",
18
+ "KAPO_LAZY_MODEL_STARTUP": "1",
19
+ "KAPO_LAZY_EMBED_STARTUP": "1",
20
+ "MODEL_PROFILE_ID": "hf-debugger-qwen25-7b-instruct",
21
+ "MODEL_REPO": "Qwen/Qwen2.5-1.5B-Instruct",
22
+ "BRAIN_ROLES": "fallback",
23
+ "BRAIN_LANGUAGES": "ar,en",
24
+ "BRAIN_PLATFORM_NAME": "hf_debugger_main",
25
+ "BRAIN_TEMPLATE": "hf-space-cpu",
26
+ "BRAIN_PROVIDER": "huggingface",
27
+ "FIREBASE_ENABLED": "1",
28
+ "FIREBASE_PROJECT_ID": "citadel4travels",
29
+ "FIREBASE_NAMESPACE": "kapo",
30
+ }
31
+
32
+
33
+ def _copy_tree(source: Path, target: Path) -> None:
34
+ if target.exists():
35
+ shutil.rmtree(target, ignore_errors=True)
36
+ shutil.copytree(
37
+ source,
38
+ target,
39
+ ignore=shutil.ignore_patterns('__pycache__', '*.pyc', '.git', '.venv'),
40
+ )
41
+
42
+
43
+ def _merge_overlay(overlay_root: Path, runtime_root: Path) -> None:
44
+ if not overlay_root.exists():
45
+ return
46
+ for path in sorted(overlay_root.rglob('*')):
47
+ rel = path.relative_to(overlay_root)
48
+ dst = runtime_root / rel
49
+ if path.is_dir():
50
+ dst.mkdir(parents=True, exist_ok=True)
51
+ continue
52
+ dst.parent.mkdir(parents=True, exist_ok=True)
53
+ shutil.copy2(path, dst)
54
+
55
+
56
+ def main() -> None:
57
+ source_root = Path(os.getenv('KAPO_SPACE_SOURCE_ROOT', '/app')).resolve()
58
+ default_root = Path('/data/kapo_runtime/current') if Path('/data').exists() else Path('/tmp/kapo_runtime/current')
59
+ runtime_root = Path(os.getenv('KAPO_RUNTIME_ROOT', str(default_root))).resolve()
60
+ overlay_root = Path(os.getenv('KAPO_OVERLAY_ROOT', str(runtime_root.parent / 'overlay'))).resolve()
61
+ runtime_pkg = runtime_root / 'brain_server'
62
+ source_pkg = source_root / 'brain_server'
63
+ for key, value in DEFAULT_ENV.items():
64
+ os.environ.setdefault(str(key), str(value))
65
+ runtime_root.mkdir(parents=True, exist_ok=True)
66
+ overlay_root.mkdir(parents=True, exist_ok=True)
67
+ if not runtime_pkg.exists():
68
+ _copy_tree(source_pkg, runtime_pkg)
69
+ _merge_overlay(overlay_root, runtime_root)
70
+ os.environ['KAPO_RUNTIME_ROOT'] = str(runtime_root)
71
+ os.environ['KAPO_SYNC_ROOT'] = str(runtime_root)
72
+ os.environ['KAPO_OVERLAY_ROOT'] = str(overlay_root)
73
+ port = str(os.getenv('PORT', '7860') or '7860')
74
+ os.execvp(
75
+ sys.executable,
76
+ [
77
+ sys.executable,
78
+ '-m',
79
+ 'uvicorn',
80
+ 'api.main:app',
81
+ '--host',
82
+ '0.0.0.0',
83
+ '--port',
84
+ port,
85
+ '--app-dir',
86
+ str(runtime_pkg),
87
+ ],
88
+ )
89
+
90
+
91
+ if __name__ == '__main__':
92
+ main()
brain_server/.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
brain_server/Dockerfile ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # تثبيت الأدوات الأساسية
6
+ RUN apt-get update && apt-get install -y --no-install-recommends \
7
+ build-essential \
8
+ curl \
9
+ git \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ # نسخ requirements
13
+ COPY requirements.txt /app/requirements.txt
14
+
15
+ # تثبيت المكتبات
16
+ RUN pip install --no-cache-dir -r /app/requirements.txt
17
+
18
+ # نسخ المشروع بحيث يعمل المساران: /app/api و /app/brain_server/api
19
+ COPY . /app/brain_server/
20
+ COPY . /app/
21
+
22
+ ENV PYTHONUNBUFFERED=1
23
+
24
+ EXPOSE 7860
25
+
26
+ ENV PYTHONPATH=/app:/app/brain_server
27
+
28
+ CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "7860"]
brain_server/README.md ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Ai Brain
3
+ emoji: 🚀
4
+ colorFrom: pink
5
+ colorTo: red
6
+ sdk: docker
7
+ pinned: false
8
+ ---
9
+
10
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
brain_server/agents/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """???? ???????."""
brain_server/agents/auto_heal_agent.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Auto-Heal Agent: ????? ?????? ??????? ???????."""
2
+ import logging
3
+ from typing import Dict, Any
4
+ from api.deps import get_logger
5
+
6
+ logger = get_logger("kapo.agent.auto_heal")
7
+
8
+ ERROR_MAP = {
9
+ "ModuleNotFoundError": "pip install <package>",
10
+ "command not found": "apt-get install <package>",
11
+ "SyntaxError": "???? ????? ????? ????? ????????",
12
+ "Permission denied": "???? ?? ??????? ?????? ?? ?????? ???? ?????",
13
+ }
14
+
15
+
16
+ class AutoHealAgent:
17
+ def suggest(self, error_text: str, step: Dict[str, Any]) -> Dict[str, Any]:
18
+ """?????? ??? ????? ???? ??? ?????."""
19
+ for key, fix in ERROR_MAP.items():
20
+ if key in error_text:
21
+ return {"error": error_text, "suggested_fix": fix, "step": step}
22
+ return {"error": error_text, "suggested_fix": "???? ?? ????? ??????"}
brain_server/agents/memory_agent.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Memory Agent: ?????? ?????? ???????."""
2
+ import logging
3
+ from typing import Any, Dict
4
+ from memory.short_term import ShortTermMemory
5
+ from memory.episodic_db import EpisodicDB
6
+ from memory.knowledge_vector import KnowledgeVectorStore
7
+ from api.deps import get_logger, is_remote_brain_only
8
+ from agents.supervisor_agent import AgentOS
9
+
10
+ logger = get_logger("kapo.agent.memory")
11
+
12
+
13
+ class MemoryAgent:
14
+ def __init__(self):
15
+ self.remote_only = is_remote_brain_only()
16
+ self.short = ShortTermMemory()
17
+ self.episodic = None if self.remote_only else EpisodicDB()
18
+ self.knowledge = None if self.remote_only else KnowledgeVectorStore()
19
+ self.agent_os = None if self.remote_only else AgentOS()
20
+
21
+ def write_short_term(self, key: str, value: Dict[str, Any]) -> None:
22
+ if self.remote_only:
23
+ return
24
+ self.short.set(key, value)
25
+
26
+ def read_short_term(self, key: str) -> Dict[str, Any] | None:
27
+ if self.remote_only:
28
+ return None
29
+ return self.short.get(key)
30
+
31
+ def store_experience(self, payload: Dict[str, Any]) -> None:
32
+ if self.remote_only or self.episodic is None:
33
+ return
34
+ self.episodic.insert_experience(
35
+ task=payload.get("task", ""),
36
+ plan=payload.get("plan", {}),
37
+ tools_used=payload.get("tools_used", {}),
38
+ result=payload.get("result", {}),
39
+ success=1 if payload.get("success") else 0,
40
+ )
41
+
42
+ def query_knowledge(self, text: str, top_k: int = 3):
43
+ if self.remote_only or self.knowledge is None:
44
+ return []
45
+ return self.knowledge.query(text, top_k=top_k)
46
+
47
+ def run_agent_os(self):
48
+ """????? ???? ??????? ?????? ??????."""
49
+ if self.remote_only or self.agent_os is None:
50
+ return {"skipped": True, "reason": "REMOTE_BRAIN_ONLY"}
51
+ return self.agent_os.run()
brain_server/agents/planner_agent.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Planner agent that builds structured execution-friendly plans."""
2
+
3
+ from typing import Any
4
+
5
+ from api.deps import get_logger
6
+
7
+ logger = get_logger("kapo.agent.planner")
8
+
9
+
10
+ def _contains_any(text: str, terms: tuple[str, ...]) -> bool:
11
+ lowered = text.lower()
12
+ return any(term in lowered for term in terms)
13
+
14
+
15
+ class PlannerAgent:
16
+ def _base_step(self, step_id: str, action: str, user_input: str, tool_hint: str, role: str) -> dict[str, Any]:
17
+ return {
18
+ "id": step_id,
19
+ "action": action,
20
+ "input": user_input,
21
+ "tool_hint": tool_hint,
22
+ "role": role,
23
+ }
24
+
25
+ def run(self, user_input: str, context: dict[str, Any]) -> list[dict[str, Any]]:
26
+ try:
27
+ text = (user_input or "").strip()
28
+ if not text:
29
+ return [self._base_step("step-1", "respond", "Empty request", "python", "chat")]
30
+
31
+ context = context or {}
32
+ steps: list[dict[str, Any]] = [
33
+ {
34
+ **self._base_step("step-1", "analyze", text, "python", "planner"),
35
+ "context_keys": sorted(context.keys()),
36
+ }
37
+ ]
38
+
39
+ research_terms = ("search", "research", "browse", "look up", "find out", "ابحث", "بحث", "دور", "فتش")
40
+ coding_terms = (
41
+ "build", "fix", "debug", "refactor", "implement", "generate", "write code", "api", "fastapi", "python",
42
+ "react", "repo", "project", "اصلح", "نفذ", "شغل", "عدل", "ابني", "برمجة", "كود", "مشروع",
43
+ )
44
+ planning_terms = ("plan", "roadmap", "architecture", "analyze", "design", "structure", "خطة", "بنية", "معمارية", "حلل")
45
+ explain_terms = ("explain", "describe", "summarize", "اشرح", "وضح", "لخص", "عرفني")
46
+
47
+ is_research = _contains_any(text, research_terms)
48
+ is_coding = _contains_any(text, coding_terms)
49
+ is_planning = _contains_any(text, planning_terms)
50
+ is_explainer = _contains_any(text, explain_terms)
51
+
52
+ if is_research:
53
+ steps.extend(
54
+ [
55
+ self._base_step("step-2", "research", text, "web", "chat"),
56
+ self._base_step("step-3", "synthesize", "Summarize and cite the most relevant findings", "python", "supervisor"),
57
+ ]
58
+ )
59
+ elif is_coding:
60
+ steps.extend(
61
+ [
62
+ self._base_step("step-2", "collect_requirements", "Inspect impacted files, dependencies, and constraints", "python", "planner"),
63
+ self._base_step("step-3", "execute", text, "python", "coding"),
64
+ self._base_step("step-4", "verify", "Run validation checks and inspect resulting output", "python", "coding"),
65
+ self._base_step("step-5", "summarize", "Summarize what changed, risks, and verification status", "python", "supervisor"),
66
+ ]
67
+ )
68
+ elif is_planning:
69
+ steps.extend(
70
+ [
71
+ self._base_step("step-2", "decompose", "Break the request into phases, dependencies, and risks", "python", "planner"),
72
+ self._base_step("step-3", "respond", "Provide a structured implementation plan", "python", "supervisor"),
73
+ ]
74
+ )
75
+ elif is_explainer:
76
+ steps.extend(
77
+ [
78
+ self._base_step("step-2", "collect_context", "Gather the minimum relevant context for explanation", "python", "chat"),
79
+ self._base_step("step-3", "respond", "Explain the topic clearly and directly", "python", "supervisor"),
80
+ ]
81
+ )
82
+ else:
83
+ steps.append(self._base_step("step-2", "respond", text, "python", "chat"))
84
+
85
+ return steps
86
+ except Exception as exc:
87
+ logger.exception("Planner failed")
88
+ return [{"id": "step-err", "action": "error", "input": str(exc), "tool_hint": "python", "role": "fallback"}]
brain_server/agents/reasoning_agent.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Reasoning Agent: ???? ????? ????? ?????."""
2
+ import logging
3
+ from typing import List, Dict, Any
4
+ from api.deps import get_logger
5
+
6
+ logger = get_logger("kapo.agent.reasoning")
7
+
8
+
9
+ class ReasoningAgent:
10
+ def run(self, user_input: str, plan: List[Dict[str, Any]]) -> Dict[str, Any]:
11
+ try:
12
+ logger.info("Reasoning", extra={"component": "reasoning"})
13
+ return {
14
+ "summary": "?? ????? ????? ??? ????? ????? ??????.",
15
+ "steps_count": len(plan),
16
+ "input": user_input,
17
+ }
18
+ except Exception as exc:
19
+ logger.exception("Reasoning failed")
20
+ return {"error": str(exc)}
brain_server/agents/supervisor_agent.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Supervisor Agent: ????? ??????? ????? ??????."""
2
+ import logging
3
+ from typing import Any, Dict, List
4
+ from api.deps import get_logger
5
+ from memory.episodic_db import EpisodicDB
6
+
7
+ logger = get_logger("kapo.agent.supervisor")
8
+
9
+
10
+ class SupervisorAgent:
11
+ def review(self, results: List[Dict[str, Any]]) -> Dict[str, Any]:
12
+ try:
13
+ success = all(r.get("exit_code", 0) == 0 for r in results if isinstance(r, dict))
14
+ return {"success": success, "results_count": len(results)}
15
+ except Exception as exc:
16
+ logger.exception("Supervisor failed")
17
+ return {"success": False, "error": str(exc)}
18
+
19
+
20
+ class AgentOS:
21
+ """Self-Improving loop reading episodic DB and proposing prompt updates."""
22
+
23
+ def run(self) -> Dict[str, Any]:
24
+ try:
25
+ db = EpisodicDB()
26
+ recent = db.list_recent(limit=20)
27
+ success_rate = 0
28
+ if recent:
29
+ success_rate = sum(1 for r in recent if r.get("success")) / len(recent)
30
+ proposal = {
31
+ "summary": f"Recent success rate: {success_rate:.2f}",
32
+ "prompt_update": "?????? ????? ????? Planner ??? ?????? ??????.",
33
+ "sandbox_test": "Run simulated plan execution in sandbox before promotion.",
34
+ }
35
+ return proposal
36
+ except Exception as exc:
37
+ logger.exception("AgentOS failed")
38
+ return {"error": str(exc)}
brain_server/agents/tool_selector_agent.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tool selector for remote execution steps."""
2
+ import sqlite3
3
+ from typing import Any
4
+
5
+ from api.deps import get_logger, load_config
6
+
7
+ logger = get_logger("kapo.agent.tool_selector")
8
+
9
+
10
+ class ToolSelectorAgent:
11
+ def __init__(self):
12
+ self.cfg = load_config()
13
+
14
+ def _load_catalog(self) -> list[dict[str, Any]]:
15
+ tools_db = self.cfg.get("TOOLS_DB_PATH")
16
+ if not tools_db or not str(tools_db).endswith(".db"):
17
+ return []
18
+
19
+ try:
20
+ conn = sqlite3.connect(str(tools_db))
21
+ cur = conn.cursor()
22
+ cur.execute(
23
+ """
24
+ SELECT tool_name, install_command, path, description, installed
25
+ FROM tools
26
+ """
27
+ )
28
+ rows = cur.fetchall()
29
+ conn.close()
30
+ except sqlite3.OperationalError:
31
+ return []
32
+ except Exception:
33
+ logger.exception("Tool catalog load failed")
34
+ return []
35
+
36
+ return [
37
+ {
38
+ "tool_name": row[0],
39
+ "install_command": row[1],
40
+ "path": row[2],
41
+ "description": row[3] or "",
42
+ "installed": bool(row[4]),
43
+ }
44
+ for row in rows
45
+ ]
46
+
47
+ def _step_text(self, step: dict[str, Any]) -> str:
48
+ for key in ("input", "description", "title", "summary", "task", "prompt"):
49
+ value = str(step.get(key, "")).strip()
50
+ if value:
51
+ return value
52
+ return ""
53
+
54
+ def _fallback_tool(self, tool_hint: str) -> dict[str, Any]:
55
+ name = f"fallback_{tool_hint or 'command'}"
56
+ return {
57
+ "tool_name": name,
58
+ "description": "Synthetic fallback tool selected by ToolSelectorAgent",
59
+ "path": tool_hint or "",
60
+ "installed": True,
61
+ }
62
+
63
+ def _fallback_command(self, step: dict[str, Any], tool_hint: str = "") -> str:
64
+ action = str(step.get("action", "")).strip().lower()
65
+ step_input = self._step_text(step)
66
+ explicit_command = str(step.get("command", "")).strip()
67
+ if explicit_command:
68
+ return explicit_command
69
+ if action == "research":
70
+ return f"echo Research task queued: {step_input}".strip()
71
+ if action in {"verify", "summarize", "respond", "synthesize"}:
72
+ if step_input:
73
+ return f"python -c \"print({step_input!r})\""
74
+ return "echo Verification requested"
75
+ if action in {"execute", "collect_requirements", "analyze", "decompose", "collect_context"}:
76
+ return f"python -c \"print({step_input!r})\"" if step_input else "python -c \"print('step received')\""
77
+ if tool_hint in {"python", "py"} or not tool_hint:
78
+ return f"python -c \"print({step_input!r})\"" if step_input else "python -c \"print('step received')\""
79
+ if step_input:
80
+ return f"echo {step_input}"
81
+ return "echo Step received"
82
+
83
+ def select_tool(self, step: dict[str, Any]) -> dict[str, Any]:
84
+ action = str(step.get("action", "")).strip().lower()
85
+ tool_hint = str(step.get("tool_hint", "")).strip().lower()
86
+ step_input = self._step_text(step).lower()
87
+ catalog = self._load_catalog()
88
+
89
+ for tool in catalog:
90
+ haystack = " ".join([tool["tool_name"], tool["description"], tool["path"] or ""]).lower()
91
+ if tool_hint and tool_hint in haystack:
92
+ return {"command": tool["path"] or tool["tool_name"], "files": {}, "env": {}, "tool": tool}
93
+ if action and action in haystack:
94
+ return {"command": tool["path"] or tool["tool_name"], "files": {}, "env": {}, "tool": tool}
95
+ if step_input and any(token in haystack for token in step_input.split()[:4]):
96
+ return {"command": tool["path"] or tool["tool_name"], "files": {}, "env": {}, "tool": tool}
97
+
98
+ return {
99
+ "command": self._fallback_command(step, tool_hint=tool_hint),
100
+ "files": step.get("files", {}),
101
+ "env": step.get("env", {}),
102
+ "tool": self._fallback_tool(tool_hint or "python"),
103
+ }
brain_server/api/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """API package."""
brain_server/api/deps.py ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Common dependency utilities for Brain API."""
2
+ import logging
3
+ import logging.config
4
+ import os
5
+ import re
6
+ from typing import Any
7
+
8
+ import yaml
9
+ from dotenv import load_dotenv
10
+
11
+ CONFIG_CACHE: dict[str, Any] | None = None
12
+ _LOGGING_READY = False
13
+ PLACEHOLDER_RE = re.compile(r"^\$\{[A-Z0-9_]+\}$")
14
+
15
+
16
+ def _normalize_config_paths(cfg: dict[str, Any]) -> dict[str, Any]:
17
+ if os.name != "nt":
18
+ return cfg
19
+
20
+ root = os.getcwd()
21
+ normalized = dict(cfg)
22
+ path_keys = {
23
+ "DB_PATH",
24
+ "TOOLS_DB_PATH",
25
+ "FAISS_INDEX_PATH",
26
+ "BRAIN_LOG_PATH",
27
+ "EXEC_LOG_PATH",
28
+ "LOCAL_DATA_DIR",
29
+ }
30
+ for key in path_keys:
31
+ value = normalized.get(key)
32
+ if not isinstance(value, str) or not value:
33
+ continue
34
+ if value.startswith("/data"):
35
+ normalized[key] = os.path.join(root, "data", value[len("/data"):].lstrip("/\\"))
36
+ elif value.startswith("/models"):
37
+ normalized[key] = os.path.join(root, "models", value[len("/models"):].lstrip("/\\"))
38
+ return normalized
39
+
40
+
41
+ def _strip_unresolved_placeholders(value):
42
+ if isinstance(value, dict):
43
+ return {key: _strip_unresolved_placeholders(item) for key, item in value.items()}
44
+ if isinstance(value, list):
45
+ return [_strip_unresolved_placeholders(item) for item in value]
46
+ if isinstance(value, str) and PLACEHOLDER_RE.match(value.strip()):
47
+ return ""
48
+ return value
49
+
50
+
51
+ def load_config() -> dict:
52
+ global CONFIG_CACHE
53
+ if CONFIG_CACHE is not None:
54
+ return CONFIG_CACHE
55
+
56
+ load_dotenv()
57
+ config_path = os.path.join(os.path.dirname(__file__), "..", "config", "config.yaml")
58
+ with open(config_path, "r", encoding="utf-8") as handle:
59
+ raw = handle.read()
60
+
61
+ for key, value in os.environ.items():
62
+ raw = raw.replace(f"${{{key}}}", value)
63
+
64
+ parsed = yaml.safe_load(raw) or {}
65
+ CONFIG_CACHE = _normalize_config_paths(_strip_unresolved_placeholders(parsed))
66
+ return CONFIG_CACHE
67
+
68
+
69
+ def is_remote_brain_only() -> bool:
70
+ cfg = load_config()
71
+ value = cfg.get("REMOTE_BRAIN_ONLY", os.getenv("REMOTE_BRAIN_ONLY", "0"))
72
+ return str(value).strip().lower() in {"1", "true", "yes", "on"}
73
+
74
+
75
+ def setup_logging() -> None:
76
+ global _LOGGING_READY
77
+ if _LOGGING_READY:
78
+ return
79
+
80
+ log_cfg_path = os.path.join(os.path.dirname(__file__), "..", "config", "logging.yaml")
81
+ if not os.path.exists(log_cfg_path):
82
+ logging.basicConfig(level=logging.INFO)
83
+ _LOGGING_READY = True
84
+ return
85
+
86
+ try:
87
+ with open(log_cfg_path, "r", encoding="utf-8") as handle:
88
+ cfg = yaml.safe_load(handle) or {}
89
+ logging.config.dictConfig(cfg)
90
+ except Exception:
91
+ logging.basicConfig(level=logging.INFO)
92
+ logging.getLogger("kapo").warning("Falling back to basic logging configuration")
93
+
94
+ _LOGGING_READY = True
95
+
96
+
97
+ def get_logger(name: str) -> logging.Logger:
98
+ setup_logging()
99
+ return logging.getLogger(name)
100
+
101
+
102
+ def _normalize_base_url(candidate: Any) -> str:
103
+ text = "" if candidate is None else str(candidate).strip()
104
+ if not text:
105
+ return ""
106
+ if "://" not in text:
107
+ text = f"http://{text}"
108
+ return text.rstrip("/")
109
+
110
+
111
+ def get_executor_url(cfg: dict) -> str:
112
+ env_url = _normalize_base_url(os.getenv("EXECUTOR_URL"))
113
+ if env_url:
114
+ return env_url
115
+
116
+ cfg_url = _normalize_base_url(cfg.get("EXECUTOR_URL"))
117
+ if cfg_url:
118
+ return cfg_url
119
+
120
+ scheme = str(cfg.get("EXECUTOR_SCHEME") or os.getenv("EXECUTOR_SCHEME", "http")).strip() or "http"
121
+ host = str(cfg.get("EXECUTOR_HOST") or os.getenv("EXECUTOR_HOST", "localhost")).strip()
122
+ port = str(cfg.get("EXECUTOR_PORT") or os.getenv("EXECUTOR_PORT", "9000")).strip()
123
+
124
+ if "://" in host:
125
+ return host.rstrip("/")
126
+ if ":" in host:
127
+ return f"{scheme}://{host}".rstrip("/")
128
+ return f"{scheme}://{host}:{port}".rstrip("/")
129
+
130
+
131
+ def get_executor_headers(cfg: dict) -> dict:
132
+ header = cfg.get("EXECUTOR_BYPASS_HEADER") or os.getenv("EXECUTOR_BYPASS_HEADER")
133
+ value = cfg.get("EXECUTOR_BYPASS_VALUE") or os.getenv("EXECUTOR_BYPASS_VALUE")
134
+ if header and value:
135
+ return {str(header): str(value)}
136
+ return {}
137
+
138
+
139
+ def get_brain_headers(cfg: dict) -> dict:
140
+ header = cfg.get("BRAIN_BYPASS_HEADER") or os.getenv("BRAIN_BYPASS_HEADER")
141
+ value = cfg.get("BRAIN_BYPASS_VALUE") or os.getenv("BRAIN_BYPASS_VALUE")
142
+ if header and value:
143
+ return {str(header): str(value)}
144
+ return {}
brain_server/api/firebase_store.py ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Optional Firebase mirror for brain runtime state."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ import os
8
+ import time
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+
13
+ class FirebaseStore:
14
+ def __init__(self, component: str, logger_name: str = "kapo.brain.firebase") -> None:
15
+ self.component = component
16
+ self.logger = logging.getLogger(logger_name)
17
+ self._db = None
18
+ self._cache: dict[str, tuple[float, Any]] = {}
19
+
20
+ def enabled(self) -> bool:
21
+ return str(os.getenv("FIREBASE_ENABLED", "0")).strip().lower() in {"1", "true", "yes", "on"}
22
+
23
+ def namespace(self) -> str:
24
+ return str(os.getenv("FIREBASE_NAMESPACE", "kapo")).strip() or "kapo"
25
+
26
+ def _service_payload(self) -> dict[str, Any] | None:
27
+ raw = str(os.getenv("FIREBASE_SERVICE_ACCOUNT_JSON", "")).strip()
28
+ if not raw:
29
+ return None
30
+ try:
31
+ return json.loads(raw)
32
+ except Exception:
33
+ self.logger.exception("Invalid Firebase service account JSON")
34
+ return None
35
+
36
+ @staticmethod
37
+ def _service_path() -> str:
38
+ return str(os.getenv("FIREBASE_SERVICE_ACCOUNT_PATH", "")).strip()
39
+
40
+ def _client(self):
41
+ if not self.enabled():
42
+ return None
43
+ if self._db is not None:
44
+ return self._db
45
+ try:
46
+ import firebase_admin
47
+ from firebase_admin import credentials, firestore
48
+
49
+ if not firebase_admin._apps:
50
+ payload = self._service_payload()
51
+ if payload:
52
+ cred = credentials.Certificate(payload)
53
+ else:
54
+ service_path = self._service_path()
55
+ if not service_path:
56
+ return None
57
+ path_obj = Path(service_path).expanduser()
58
+ if not path_obj.exists() or not path_obj.is_file():
59
+ self.logger.warning(
60
+ "Firebase service account path is unavailable on this runtime: %s",
61
+ service_path,
62
+ )
63
+ return None
64
+ cred = credentials.Certificate(str(path_obj.resolve()))
65
+ options = {}
66
+ project_id = str(os.getenv("FIREBASE_PROJECT_ID", "")).strip()
67
+ if project_id:
68
+ options["projectId"] = project_id
69
+ firebase_admin.initialize_app(cred, options or None)
70
+ self._db = firestore.client()
71
+ return self._db
72
+ except Exception:
73
+ self.logger.exception("Failed to initialize Firebase client")
74
+ return None
75
+
76
+ def _collection(self, name: str) -> str:
77
+ return f"{self.namespace()}_{name}"
78
+
79
+ @staticmethod
80
+ def _safe_id(value: str, default: str = "default") -> str:
81
+ text = str(value or "").strip() or default
82
+ return "".join(ch if ch.isalnum() or ch in {"-", "_", "."} else "_" for ch in text)[:180]
83
+
84
+ def get_document(self, collection: str, doc_id: str, ttl_sec: float = 12.0) -> dict[str, Any]:
85
+ db = self._client()
86
+ if db is None:
87
+ return {}
88
+ safe_doc = self._safe_id(doc_id)
89
+ cache_key = f"{collection}:{safe_doc}"
90
+ now = time.time()
91
+ cached = self._cache.get(cache_key)
92
+ if cached and (now - cached[0]) < ttl_sec:
93
+ return dict(cached[1] or {})
94
+ try:
95
+ snapshot = db.collection(self._collection(collection)).document(safe_doc).get()
96
+ payload = snapshot.to_dict() if snapshot.exists else {}
97
+ self._cache[cache_key] = (now, payload)
98
+ return dict(payload or {})
99
+ except Exception:
100
+ self.logger.exception("Failed to read Firebase document %s/%s", collection, safe_doc)
101
+ return {}
102
+
103
+ def set_document(self, collection: str, doc_id: str, payload: dict[str, Any], merge: bool = True) -> bool:
104
+ db = self._client()
105
+ if db is None:
106
+ return False
107
+ safe_doc = self._safe_id(doc_id)
108
+ try:
109
+ body = dict(payload or {})
110
+ body["component"] = self.component
111
+ body["updated_at"] = time.time()
112
+ db.collection(self._collection(collection)).document(safe_doc).set(body, merge=merge)
113
+ self._cache.pop(f"{collection}:{safe_doc}", None)
114
+ return True
115
+ except Exception:
116
+ self.logger.exception("Failed to write Firebase document %s/%s", collection, safe_doc)
117
+ return False
118
+
119
+ def list_documents(self, collection: str, limit: int = 200) -> list[dict[str, Any]]:
120
+ db = self._client()
121
+ if db is None:
122
+ return []
123
+ try:
124
+ docs = db.collection(self._collection(collection)).limit(max(1, int(limit))).stream()
125
+ items: list[dict[str, Any]] = []
126
+ for doc in docs:
127
+ payload = doc.to_dict() or {}
128
+ payload.setdefault("id", doc.id)
129
+ items.append(payload)
130
+ return items
131
+ except Exception:
132
+ self.logger.exception("Failed to list Firebase collection %s", collection)
133
+ return []
134
+
135
+ def delete_document(self, collection: str, doc_id: str) -> bool:
136
+ db = self._client()
137
+ if db is None:
138
+ return False
139
+ safe_doc = self._safe_id(doc_id)
140
+ try:
141
+ db.collection(self._collection(collection)).document(safe_doc).delete()
142
+ self._cache.pop(f"{collection}:{safe_doc}", None)
143
+ return True
144
+ except Exception:
145
+ self.logger.exception("Failed to delete Firebase document %s/%s", collection, safe_doc)
146
+ return False
brain_server/api/main.py ADDED
@@ -0,0 +1,1977 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FastAPI entrypoint for the Brain Server."""
2
+ import gc
3
+ import logging
4
+ import os
5
+ import re
6
+ import shutil
7
+ import subprocess
8
+ import sys
9
+ import tempfile
10
+ import threading
11
+ import time
12
+ import zipfile
13
+ from collections import deque
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ import requests
18
+ from fastapi import FastAPI, File, UploadFile
19
+ from pydantic import BaseModel
20
+
21
+ from agents.memory_agent import MemoryAgent
22
+ from agents.planner_agent import PlannerAgent
23
+ from agents.reasoning_agent import ReasoningAgent
24
+ try:
25
+ from api import deps as deps_module
26
+ from api.deps import get_executor_headers, get_logger, load_config
27
+ from api.firebase_store import FirebaseStore
28
+ from api.routes_analyze import router as analyze_router
29
+ from api.routes_execute import router as execute_router
30
+ from api.routes_plan import router as plan_router
31
+ except ImportError:
32
+ from . import deps as deps_module
33
+ from .deps import get_executor_headers, get_logger, load_config
34
+ from .firebase_store import FirebaseStore
35
+ from .routes_analyze import router as analyze_router
36
+ from .routes_execute import router as execute_router
37
+ from .routes_plan import router as plan_router
38
+
39
+ logger = get_logger("kapo.brain.main")
40
+
41
+
42
+ def _configure_windows_utf8() -> None:
43
+ if os.name != "nt":
44
+ return
45
+ os.environ.setdefault("PYTHONUTF8", "1")
46
+ os.environ.setdefault("PYTHONIOENCODING", "utf-8")
47
+ os.environ.setdefault("PYTHONLEGACYWINDOWSSTDIO", "utf-8")
48
+ try:
49
+ import ctypes
50
+
51
+ kernel32 = ctypes.windll.kernel32
52
+ kernel32.SetConsoleCP(65001)
53
+ kernel32.SetConsoleOutputCP(65001)
54
+ except Exception:
55
+ pass
56
+
57
+
58
+ _configure_windows_utf8()
59
+
60
+ if hasattr(sys.stdout, "reconfigure"):
61
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
62
+ if hasattr(sys.stderr, "reconfigure"):
63
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace")
64
+
65
+ app = FastAPI(title="KAPO-AI Brain Server", version="1.0.0")
66
+ app.include_router(plan_router)
67
+ app.include_router(execute_router)
68
+ app.include_router(analyze_router)
69
+
70
+ MODEL = None
71
+ MODEL_ERROR = None
72
+ MODEL_META = {"repo_id": None, "filename": None, "path": None}
73
+ EMBED_MODEL = None
74
+ FIREBASE = FirebaseStore("brain", logger_name="kapo.brain.firebase")
75
+ FIREBASE_RUNTIME_CACHE: dict[str, tuple[float, Any]] = {}
76
+ RUNTIME_LOG_BUFFER: deque[dict[str, Any]] = deque(maxlen=200)
77
+ LAST_BRAIN_URL_REPORT: dict[str, Any] = {"url": "", "ts": 0.0}
78
+
79
+ DEFAULT_MODEL_REPO = "QuantFactory/aya-expanse-8b-GGUF"
80
+ DEFAULT_MODEL_FILE = "aya-expanse-8b.Q4_K_M.gguf"
81
+ DEFAULT_MODEL_PROFILE_ID = "supervisor-ar-en-default"
82
+ HAS_MULTIPART = True
83
+ try:
84
+ import multipart # noqa: F401
85
+ except Exception:
86
+ HAS_MULTIPART = False
87
+
88
+
89
+ class RuntimeLogHandler(logging.Handler):
90
+ def emit(self, record) -> None:
91
+ try:
92
+ RUNTIME_LOG_BUFFER.append(
93
+ {
94
+ "ts": time.time(),
95
+ "level": record.levelname,
96
+ "name": record.name,
97
+ "message": record.getMessage(),
98
+ }
99
+ )
100
+ except Exception:
101
+ pass
102
+
103
+
104
+ _runtime_log_handler = RuntimeLogHandler(level=logging.WARNING)
105
+ if not any(isinstance(handler, RuntimeLogHandler) for handler in logger.handlers):
106
+ logger.addHandler(_runtime_log_handler)
107
+
108
+
109
+ def _feature_enabled(name: str, default: bool = False) -> bool:
110
+ value = os.getenv(name)
111
+ if value is None or str(value).strip() == "":
112
+ return default
113
+ return str(value).strip().lower() in {"1", "true", "yes", "on"}
114
+
115
+
116
+ def _remote_brain_only() -> bool:
117
+ return _feature_enabled("REMOTE_BRAIN_ONLY", default=False)
118
+
119
+
120
+ def _ngrok_bootstrap_enabled() -> bool:
121
+ return _feature_enabled("BRAIN_AUTO_NGROK", default=True)
122
+
123
+
124
+ def _configured_public_url() -> str:
125
+ return str(os.getenv("BRAIN_PUBLIC_URL", "")).strip().rstrip("/")
126
+
127
+
128
+ def _reuse_public_url_on_restart() -> bool:
129
+ return _feature_enabled("BRAIN_REUSE_PUBLIC_URL_ON_RESTART", default=True)
130
+
131
+
132
+ def _auto_publish_public_url_on_startup() -> bool:
133
+ return _feature_enabled("BRAIN_AUTO_PUBLISH_URL_ON_STARTUP", default=True)
134
+
135
+
136
+ def _internal_restart_in_progress() -> bool:
137
+ return _feature_enabled("KAPO_INTERNAL_RESTART", default=False)
138
+
139
+
140
+ def _executor_connect_timeout() -> float:
141
+ return float(os.getenv("EXECUTOR_CONNECT_TIMEOUT_SEC", "3.0") or 3.0)
142
+
143
+
144
+ def _executor_read_timeout(name: str, default: float) -> float:
145
+ return float(os.getenv(name, str(default)) or default)
146
+
147
+
148
+ def _executor_roundtrip_allowed(feature_name: str, default: bool = True) -> bool:
149
+ executor_url = os.getenv("EXECUTOR_URL", "").strip()
150
+ if not executor_url:
151
+ return False
152
+ kaggle_defaults = {
153
+ "BRAIN_REMOTE_TRACE_STORE_ENABLED": False,
154
+ "BRAIN_REMOTE_AUTO_INGEST_ENABLED": False,
155
+ "BRAIN_REMOTE_STYLE_PROFILE_ENABLED": False,
156
+ }
157
+ effective_default = kaggle_defaults.get(feature_name, default) if _is_kaggle_runtime() else default
158
+ return _feature_enabled(feature_name, default=effective_default)
159
+
160
+
161
+ def _should_report_brain_url(public_url: str) -> bool:
162
+ normalized = str(public_url or "").strip().rstrip("/")
163
+ if not normalized:
164
+ return False
165
+ interval_sec = max(30.0, float(os.getenv("BRAIN_REPORT_MIN_INTERVAL_SEC", "600") or 600))
166
+ previous_url = str(LAST_BRAIN_URL_REPORT.get("url") or "").strip()
167
+ previous_ts = float(LAST_BRAIN_URL_REPORT.get("ts") or 0.0)
168
+ now = time.time()
169
+ if normalized != previous_url or (now - previous_ts) >= interval_sec:
170
+ LAST_BRAIN_URL_REPORT["url"] = normalized
171
+ LAST_BRAIN_URL_REPORT["ts"] = now
172
+ return True
173
+ return False
174
+
175
+
176
+ def _download_model(repo_id: str, filename: str, hf_token: str | None = None) -> str:
177
+ from huggingface_hub import hf_hub_download
178
+
179
+ configured_cache = str(os.getenv("MODEL_CACHE_DIR", "") or "").strip()
180
+ if configured_cache:
181
+ cache_dir = configured_cache
182
+ elif _is_kaggle_runtime():
183
+ cache_dir = str((_project_root() / "models_cache").resolve())
184
+ else:
185
+ cache_dir = os.path.join(tempfile.gettempdir(), "kapo_models")
186
+ os.makedirs(cache_dir, exist_ok=True)
187
+ return hf_hub_download(repo_id=repo_id, filename=filename, cache_dir=cache_dir, token=hf_token)
188
+
189
+
190
+ def ensure_model_loaded(repo_id: str, filename: str, hf_token: str | None = None) -> None:
191
+ global MODEL, MODEL_ERROR, MODEL_META
192
+ repo_id = (repo_id or "").strip()
193
+ filename = (filename or "").strip()
194
+ if not repo_id or not filename:
195
+ MODEL = None
196
+ MODEL_ERROR = "model repo/file missing"
197
+ return
198
+
199
+ try:
200
+ model_path = _download_model(repo_id, filename, hf_token=hf_token)
201
+ except Exception as exc:
202
+ MODEL = None
203
+ MODEL_ERROR = f"model download failed: {exc}"
204
+ logger.exception("Model download failed")
205
+ return
206
+
207
+ try:
208
+ from llama_cpp import Llama
209
+
210
+ MODEL = Llama(model_path=model_path, n_ctx=4096)
211
+ MODEL_ERROR = None
212
+ MODEL_META = {"repo_id": repo_id, "filename": filename, "path": model_path}
213
+ logger.info("Loaded model %s/%s", repo_id, filename)
214
+ except Exception as exc:
215
+ MODEL = None
216
+ MODEL_ERROR = f"model load failed: {exc}"
217
+ logger.exception("Model load failed")
218
+
219
+
220
+ def _load_embed_model() -> None:
221
+ global EMBED_MODEL
222
+ if EMBED_MODEL is not None:
223
+ return
224
+
225
+ from sentence_transformers import SentenceTransformer
226
+
227
+ model_name = os.getenv("EMBED_MODEL", "sentence-transformers/all-MiniLM-L6-v2")
228
+ EMBED_MODEL = SentenceTransformer(model_name)
229
+ logger.info("Loaded embedding model %s", model_name)
230
+
231
+
232
+ def _load_default_model() -> None:
233
+ repo_id = os.getenv("MODEL_REPO", DEFAULT_MODEL_REPO)
234
+ filename = os.getenv("MODEL_FILE", DEFAULT_MODEL_FILE)
235
+ ensure_model_loaded(repo_id, filename, hf_token=os.getenv("HF_TOKEN"))
236
+
237
+
238
+ def _brain_headers() -> dict:
239
+ cfg = load_config()
240
+ return get_executor_headers(cfg)
241
+
242
+
243
+ def _project_root() -> Path:
244
+ return Path(__file__).resolve().parents[1]
245
+
246
+
247
+ def _is_kaggle_runtime() -> bool:
248
+ return "/kaggle/" in str(_project_root()).replace("\\", "/") or bool(os.getenv("KAGGLE_KERNEL_RUN_TYPE"))
249
+
250
+
251
+ def _is_hf_space_runtime() -> bool:
252
+ return str(os.getenv("HF_SPACE_DOCKER", "0")).strip().lower() in {"1", "true", "yes", "on"} or bool(os.getenv("SPACE_ID"))
253
+
254
+
255
+ def _apply_executor_settings(settings: dict[str, Any]) -> None:
256
+ for key in (
257
+ "NGROK_AUTHTOKEN",
258
+ "MODEL_REPO",
259
+ "MODEL_FILE",
260
+ "MODEL_PROFILE_ID",
261
+ "SUPERVISOR_MODEL_PROFILE_ID",
262
+ "EMBED_MODEL",
263
+ "REQUEST_TIMEOUT_SEC",
264
+ "REQUEST_RETRIES",
265
+ "CHAT_TIMEOUT_SEC",
266
+ "EXECUTOR_BYPASS_HEADER",
267
+ "EXECUTOR_BYPASS_VALUE",
268
+ "BRAIN_BYPASS_HEADER",
269
+ "BRAIN_BYPASS_VALUE",
270
+ "REMOTE_BRAIN_ONLY",
271
+ "KAGGLE_AUTO_BOOTSTRAP",
272
+ "BRAIN_AUTO_NGROK",
273
+ "BRAIN_AUTO_PUBLISH_URL_ON_STARTUP",
274
+ "BRAIN_PUBLIC_URL",
275
+ "BRAIN_REUSE_PUBLIC_URL_ON_RESTART",
276
+ "KAGGLE_SYNC_SUBDIR",
277
+ "BRAIN_ROLES",
278
+ "BRAIN_LANGUAGES",
279
+ "BRAIN_REMOTE_KNOWLEDGE_ENABLED",
280
+ "BRAIN_REMOTE_WEB_SEARCH_ENABLED",
281
+ "BRAIN_REMOTE_TRACE_STORE_ENABLED",
282
+ "BRAIN_REMOTE_AUTO_INGEST_ENABLED",
283
+ "BRAIN_LOCAL_RAG_FALLBACK_ENABLED",
284
+ "EXECUTOR_CONNECT_TIMEOUT_SEC",
285
+ "BRAIN_REMOTE_KNOWLEDGE_TIMEOUT_SEC",
286
+ "BRAIN_REMOTE_WEB_SEARCH_TIMEOUT_SEC",
287
+ "BRAIN_REMOTE_TRACE_STORE_TIMEOUT_SEC",
288
+ "BRAIN_REMOTE_AUTO_INGEST_TIMEOUT_SEC",
289
+ "FIREBASE_ENABLED",
290
+ "FIREBASE_PROJECT_ID",
291
+ "FIREBASE_SERVICE_ACCOUNT_PATH",
292
+ "FIREBASE_SERVICE_ACCOUNT_JSON",
293
+ "FIREBASE_NAMESPACE",
294
+ ):
295
+ value = settings.get(key)
296
+ if value not in (None, ""):
297
+ os.environ[key] = str(value)
298
+ deps_module.CONFIG_CACHE = None
299
+
300
+
301
+ def _apply_firebase_runtime_settings() -> None:
302
+ if not FIREBASE.enabled():
303
+ return
304
+ shared = FIREBASE.get_document("settings", "global")
305
+ runtime = FIREBASE.get_document("runtime", "executor")
306
+ role_items = FIREBASE.list_documents("roles", limit=64)
307
+ merged = {}
308
+ merged.update(shared or {})
309
+ merged.update(runtime or {})
310
+ mappings = {
311
+ "executor_public_url": "EXECUTOR_PUBLIC_URL",
312
+ "executor_url": "EXECUTOR_URL",
313
+ "current_brain_url": "BRAIN_PUBLIC_URL",
314
+ "model_repo": "MODEL_REPO",
315
+ "model_file": "MODEL_FILE",
316
+ "model_profile_id": "MODEL_PROFILE_ID",
317
+ "supervisor_model_profile_id": "SUPERVISOR_MODEL_PROFILE_ID",
318
+ "brain_roles": "BRAIN_ROLES",
319
+ "brain_languages": "BRAIN_LANGUAGES",
320
+ }
321
+ for key, env_name in mappings.items():
322
+ value = merged.get(key)
323
+ if value not in (None, ""):
324
+ os.environ[env_name] = str(value)
325
+ if role_items:
326
+ enabled_roles = [
327
+ str(item.get("name") or item.get("id") or "").strip().lower()
328
+ for item in role_items
329
+ if str(item.get("enabled", True)).strip().lower() not in {"0", "false", "no", "off"}
330
+ ]
331
+ enabled_roles = [role for role in enabled_roles if role]
332
+ if enabled_roles:
333
+ os.environ["BRAIN_ROLES"] = ",".join(dict.fromkeys(enabled_roles))
334
+ deps_module.CONFIG_CACHE = None
335
+
336
+
337
+ def _firebase_collection_cache_key(name: str) -> str:
338
+ return f"collection:{name}"
339
+
340
+
341
+ def _firebase_list_documents_cached(collection: str, ttl_sec: float = 30.0, limit: int = 200) -> list[dict[str, Any]]:
342
+ if not FIREBASE.enabled():
343
+ return []
344
+ key = _firebase_collection_cache_key(collection)
345
+ now = time.time()
346
+ cached = FIREBASE_RUNTIME_CACHE.get(key)
347
+ if cached and (now - cached[0]) < ttl_sec:
348
+ return list(cached[1] or [])
349
+ items = FIREBASE.list_documents(collection, limit=limit)
350
+ FIREBASE_RUNTIME_CACHE[key] = (now, items)
351
+ return list(items or [])
352
+
353
+
354
+ def _firebase_role_profiles() -> list[dict[str, Any]]:
355
+ items = _firebase_list_documents_cached("roles", ttl_sec=30.0, limit=64)
356
+ if items:
357
+ return items
358
+ roles = [part.strip() for part in str(os.getenv("BRAIN_ROLES", "")).split(",") if part.strip()]
359
+ return [{"name": role, "enabled": True} for role in roles]
360
+
361
+
362
+ def _firebase_runtime_snapshot() -> dict[str, Any]:
363
+ return {
364
+ "platforms": _firebase_list_documents_cached("platforms", ttl_sec=45.0, limit=64),
365
+ "models": _firebase_list_documents_cached("models", ttl_sec=45.0, limit=128),
366
+ "prompts": _firebase_list_documents_cached("prompts", ttl_sec=20.0, limit=128),
367
+ "roles": _firebase_role_profiles(),
368
+ }
369
+
370
+
371
+ def _json_safe(value: Any) -> Any:
372
+ if isinstance(value, dict):
373
+ return {str(key): _json_safe(item) for key, item in value.items()}
374
+ if isinstance(value, list):
375
+ return [_json_safe(item) for item in value]
376
+ if isinstance(value, tuple):
377
+ return [_json_safe(item) for item in value]
378
+ if isinstance(value, (str, int, float, bool)) or value is None:
379
+ return value
380
+ return str(value)
381
+
382
+
383
+ def _firebase_prompt_body(role_name: str, language: str = "en") -> str:
384
+ role_name = str(role_name or "").strip().lower()
385
+ language = str(language or "en").strip().lower()
386
+ if not role_name:
387
+ return ""
388
+ prompts = _firebase_runtime_snapshot().get("prompts", [])
389
+ exact = []
390
+ fallback = []
391
+ for item in prompts:
392
+ if str(item.get("role_name") or "").strip().lower() != role_name:
393
+ continue
394
+ if str(item.get("enabled", True)).strip().lower() in {"0", "false", "no", "off"}:
395
+ continue
396
+ item_lang = str(item.get("language") or "en").strip().lower()
397
+ body = str(item.get("body") or "").strip()
398
+ if not body:
399
+ continue
400
+ if item_lang == language:
401
+ exact.append(body)
402
+ elif item_lang == "en":
403
+ fallback.append(body)
404
+ if exact:
405
+ return exact[0]
406
+ if fallback:
407
+ return fallback[0]
408
+ return ""
409
+
410
+
411
+ def _prepare_runtime_environment() -> None:
412
+ if not _is_kaggle_runtime():
413
+ return
414
+
415
+ source_root = _project_root()
416
+ source_text = str(source_root).replace("\\", "/")
417
+ runtime_root_env = os.getenv("KAPO_RUNTIME_ROOT", "").strip()
418
+ if runtime_root_env:
419
+ runtime_root = Path(runtime_root_env).resolve()
420
+ elif source_text.startswith("/kaggle/working/"):
421
+ runtime_root = source_root.resolve()
422
+ else:
423
+ runtime_root = Path("/kaggle/working/KAPO-AI-SYSTEM").resolve()
424
+ sync_root = runtime_root
425
+ auto_bootstrap = str(os.getenv("KAGGLE_AUTO_BOOTSTRAP", "1")).strip().lower() in {"1", "true", "yes", "on"}
426
+
427
+ if auto_bootstrap and source_text.startswith("/kaggle/input/") and source_root != runtime_root:
428
+ if runtime_root.exists():
429
+ shutil.rmtree(runtime_root, ignore_errors=True)
430
+ shutil.copytree(
431
+ source_root,
432
+ runtime_root,
433
+ ignore=shutil.ignore_patterns("__pycache__", "*.pyc", ".git", ".venv"),
434
+ )
435
+ if str(runtime_root) not in sys.path:
436
+ sys.path.insert(0, str(runtime_root))
437
+
438
+ data_dir = runtime_root / "data" / "local" / "brain_runtime"
439
+ data_dir.mkdir(parents=True, exist_ok=True)
440
+ os.environ["KAPO_RUNTIME_ROOT"] = str(runtime_root)
441
+ os.environ["KAPO_SYNC_ROOT"] = str(sync_root)
442
+ os.environ["LOCAL_DATA_DIR"] = str(data_dir)
443
+ os.environ["DB_PATH"] = str(data_dir / "episodic.db")
444
+ os.environ["TOOLS_DB_PATH"] = str(data_dir / "tools.db")
445
+ os.environ["FAISS_INDEX_PATH"] = str(data_dir / "faiss.index")
446
+ os.environ["REMOTE_BRAIN_ONLY"] = str(os.getenv("REMOTE_BRAIN_ONLY", "1") or "1")
447
+ deps_module.CONFIG_CACHE = None
448
+
449
+
450
+ def _sync_target_root() -> str:
451
+ return os.getenv("KAPO_SYNC_ROOT") or os.getenv("KAPO_RUNTIME_ROOT") or os.getcwd()
452
+
453
+
454
+ def _sync_root_path() -> Path:
455
+ return Path(_sync_target_root()).resolve()
456
+
457
+
458
+ def _resolve_sync_path(user_path: str | None = None) -> Path:
459
+ root = _sync_root_path()
460
+ relative = str(user_path or "").strip().replace("\\", "/").lstrip("/")
461
+ candidate = (root / relative).resolve() if relative else root
462
+ if candidate != root and root not in candidate.parents:
463
+ raise ValueError("Path escapes sync root")
464
+ return candidate
465
+
466
+
467
+ def _describe_sync_entry(path: Path) -> dict[str, Any]:
468
+ stat = path.stat()
469
+ return {
470
+ "name": path.name or str(path),
471
+ "path": str(path.relative_to(_sync_root_path())).replace("\\", "/") if path != _sync_root_path() else "",
472
+ "is_dir": path.is_dir(),
473
+ "size": stat.st_size,
474
+ "modified_at": stat.st_mtime,
475
+ }
476
+
477
+
478
+ def _public_url_state_path() -> Path:
479
+ runtime_root = Path(_sync_target_root()).resolve()
480
+ state_dir = runtime_root / "data" / "local" / "brain_runtime"
481
+ state_dir.mkdir(parents=True, exist_ok=True)
482
+ return state_dir / "public_url.txt"
483
+
484
+
485
+ def _remember_public_url(public_url: str) -> None:
486
+ value = str(public_url or "").strip().rstrip("/")
487
+ if not value:
488
+ return
489
+ os.environ["BRAIN_PUBLIC_URL"] = value
490
+ try:
491
+ _public_url_state_path().write_text(value, encoding="utf-8")
492
+ except Exception:
493
+ logger.warning("Failed to persist public URL", exc_info=True)
494
+
495
+
496
+ def _load_saved_public_url() -> str:
497
+ configured = _configured_public_url()
498
+ if configured:
499
+ return configured
500
+ try:
501
+ value = _public_url_state_path().read_text(encoding="utf-8").strip().rstrip("/")
502
+ return value
503
+ except Exception:
504
+ return ""
505
+
506
+
507
+ def _ngrok_api_state_path() -> Path:
508
+ runtime_root = Path(_sync_target_root()).resolve()
509
+ state_dir = runtime_root / "data" / "local" / "brain_runtime"
510
+ state_dir.mkdir(parents=True, exist_ok=True)
511
+ return state_dir / "ngrok_api_url.txt"
512
+
513
+
514
+ def _remember_ngrok_api_url(api_url: str) -> None:
515
+ value = str(api_url or "").strip().rstrip("/")
516
+ if not value:
517
+ return
518
+ os.environ["KAPO_NGROK_API_URL"] = value
519
+ try:
520
+ _ngrok_api_state_path().write_text(value, encoding="utf-8")
521
+ except Exception:
522
+ logger.warning("Failed to persist ngrok API URL", exc_info=True)
523
+
524
+
525
+ def _load_saved_ngrok_api_url() -> str:
526
+ configured = str(os.getenv("KAPO_NGROK_API_URL", "")).strip().rstrip("/")
527
+ if configured:
528
+ return configured
529
+ try:
530
+ return _ngrok_api_state_path().read_text(encoding="utf-8").strip().rstrip("/")
531
+ except Exception:
532
+ return ""
533
+
534
+
535
+ def _ngrok_api_candidates() -> list[str]:
536
+ seen: set[str] = set()
537
+ candidates: list[str] = []
538
+ for candidate in [_load_saved_ngrok_api_url(), "http://127.0.0.1:4040", "http://127.0.0.1:4041", "http://127.0.0.1:4042"]:
539
+ value = str(candidate or "").strip().rstrip("/")
540
+ if value and value not in seen:
541
+ seen.add(value)
542
+ candidates.append(value)
543
+ return candidates
544
+
545
+
546
+ def _probe_ngrok_api(api_url: str) -> bool:
547
+ try:
548
+ response = requests.get(f"{api_url}/api/tunnels", timeout=2)
549
+ return response.status_code == 200
550
+ except Exception:
551
+ return False
552
+
553
+
554
+ def _find_live_ngrok_api() -> str | None:
555
+ for api_url in _ngrok_api_candidates():
556
+ if _probe_ngrok_api(api_url):
557
+ _remember_ngrok_api_url(api_url)
558
+ return api_url
559
+ return None
560
+
561
+
562
+ def _ngrok_binary_path() -> str:
563
+ env_path = str(os.getenv("NGROK_PATH", "")).strip()
564
+ if env_path and Path(env_path).exists():
565
+ return env_path
566
+ default_ngrok_path = ""
567
+ try:
568
+ from pyngrok import conf
569
+
570
+ default_ngrok_path = str(conf.get_default().ngrok_path or "").strip()
571
+ if default_ngrok_path and Path(default_ngrok_path).exists():
572
+ return default_ngrok_path
573
+ except Exception:
574
+ pass
575
+ try:
576
+ from pyngrok import installer
577
+
578
+ install_target = default_ngrok_path or str((Path.home() / ".ngrok" / "ngrok").resolve())
579
+ Path(install_target).parent.mkdir(parents=True, exist_ok=True)
580
+ installer.install_ngrok(install_target)
581
+ if Path(install_target).exists():
582
+ return install_target
583
+ except Exception:
584
+ logger.warning("Failed to auto-install ngrok binary", exc_info=True)
585
+ discovered = shutil.which("ngrok")
586
+ if discovered:
587
+ return discovered
588
+ return "ngrok"
589
+
590
+
591
+ def _ensure_ngrok_auth(token: str) -> None:
592
+ ngrok_path = _ngrok_binary_path()
593
+ subprocess.run(
594
+ [ngrok_path, "config", "add-authtoken", token],
595
+ check=False,
596
+ stdout=subprocess.DEVNULL,
597
+ stderr=subprocess.DEVNULL,
598
+ )
599
+
600
+
601
+ def _start_detached_ngrok_agent(token: str) -> str | None:
602
+ if token:
603
+ _ensure_ngrok_auth(token)
604
+ os.environ["NGROK_AUTHTOKEN"] = token
605
+
606
+ ngrok_path = _ngrok_binary_path()
607
+ popen_kwargs = {
608
+ "stdout": subprocess.DEVNULL,
609
+ "stderr": subprocess.DEVNULL,
610
+ "stdin": subprocess.DEVNULL,
611
+ }
612
+ if os.name == "nt":
613
+ popen_kwargs["creationflags"] = getattr(subprocess, "DETACHED_PROCESS", 0) | getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
614
+ else:
615
+ popen_kwargs["start_new_session"] = True
616
+
617
+ subprocess.Popen(
618
+ [ngrok_path, "start", "--none", "--log=stdout"],
619
+ **popen_kwargs,
620
+ )
621
+
622
+ deadline = time.time() + 12
623
+ while time.time() < deadline:
624
+ api_url = _find_live_ngrok_api()
625
+ if api_url:
626
+ return api_url
627
+ time.sleep(0.5)
628
+ return None
629
+
630
+
631
+ def _list_ngrok_tunnels(api_url: str) -> list[dict[str, Any]]:
632
+ response = requests.get(f"{api_url}/api/tunnels", timeout=5)
633
+ response.raise_for_status()
634
+ payload = response.json()
635
+ tunnels = payload.get("tunnels")
636
+ return tunnels if isinstance(tunnels, list) else []
637
+
638
+
639
+ def _existing_ngrok_public_url(api_url: str, port: int) -> str | None:
640
+ for tunnel in _list_ngrok_tunnels(api_url):
641
+ public_url = str(tunnel.get("public_url") or "").strip()
642
+ config = tunnel.get("config") or {}
643
+ addr = str(config.get("addr") or "").strip()
644
+ if public_url and addr.endswith(f":{port}"):
645
+ return public_url.rstrip("/")
646
+ return None
647
+
648
+
649
+ def _create_ngrok_tunnel(api_url: str, port: int) -> str | None:
650
+ response = requests.post(
651
+ f"{api_url}/api/tunnels",
652
+ json={
653
+ "name": f"http-{port}-kapo",
654
+ "addr": str(port),
655
+ "proto": "http",
656
+ },
657
+ timeout=10,
658
+ )
659
+ response.raise_for_status()
660
+ payload = response.json()
661
+ return str(payload.get("public_url") or "").strip().rstrip("/") or None
662
+
663
+
664
+ def _report_brain_url(public_url: str) -> None:
665
+ executor_url = os.getenv("EXECUTOR_URL", "").strip().rstrip("/")
666
+ if not executor_url:
667
+ return
668
+ if not _should_report_brain_url(public_url):
669
+ return
670
+ last_error: Exception | None = None
671
+ connect_timeout = max(1.0, float(os.getenv("BRAIN_REPORT_CONNECT_TIMEOUT_SEC", "3.0") or 3.0))
672
+ read_timeout = max(2.0, float(os.getenv("BRAIN_REPORT_READ_TIMEOUT_SEC", "5.0") or 5.0))
673
+ retries = max(1, int(os.getenv("BRAIN_REPORT_RETRIES", "2") or 2))
674
+ for _ in range(retries):
675
+ try:
676
+ response = requests.post(
677
+ f"{executor_url}/brain/report-url",
678
+ json={
679
+ "brain_url": public_url,
680
+ "platform": "kaggle" if _is_kaggle_runtime() else "remote",
681
+ "role": os.getenv("BRAIN_PRIMARY_ROLE", "fallback"),
682
+ "roles": [part.strip() for part in os.getenv("BRAIN_ROLES", "supervisor,chat,coding,planner,arabic,fallback").split(",") if part.strip()],
683
+ "languages": [part.strip() for part in os.getenv("BRAIN_LANGUAGES", "ar,en").split(",") if part.strip()],
684
+ "model_profile_id": os.getenv("MODEL_PROFILE_ID") or os.getenv("SUPERVISOR_MODEL_PROFILE_ID") or DEFAULT_MODEL_PROFILE_ID,
685
+ "model_repo": MODEL_META.get("repo_id") or os.getenv("MODEL_REPO") or DEFAULT_MODEL_REPO,
686
+ "model_file": MODEL_META.get("filename") or os.getenv("MODEL_FILE") or DEFAULT_MODEL_FILE,
687
+ },
688
+ headers=_brain_headers(),
689
+ timeout=(connect_timeout, read_timeout),
690
+ )
691
+ response.raise_for_status()
692
+ return
693
+ except Exception as exc:
694
+ last_error = exc
695
+ time.sleep(1)
696
+ logger.info(
697
+ "Brain URL report to executor timed out or failed; continuing (%s)",
698
+ last_error,
699
+ )
700
+
701
+
702
+ def _pull_executor_settings() -> dict[str, Any]:
703
+ executor_url = os.getenv("EXECUTOR_URL", "").strip().rstrip("/")
704
+ if not executor_url:
705
+ return {}
706
+ try:
707
+ response = requests.get(
708
+ f"{executor_url}/share/settings",
709
+ headers=_brain_headers(),
710
+ timeout=15,
711
+ )
712
+ if response.status_code == 200:
713
+ return response.json()
714
+ logger.warning("Executor settings request failed: %s", response.text)
715
+ except Exception:
716
+ logger.warning("Failed to pull executor settings", exc_info=True)
717
+ return {}
718
+
719
+
720
+ def start_ngrok(token: str | None = None) -> str | None:
721
+ restart_reuse = str(os.getenv("KAPO_RESTART_REUSE_PUBLIC_URL", "")).strip().lower() in {"1", "true", "yes", "on"}
722
+ if restart_reuse:
723
+ saved_public_url = _load_saved_public_url()
724
+ if saved_public_url:
725
+ _remember_public_url(saved_public_url)
726
+ _report_brain_url(saved_public_url)
727
+ FIREBASE.set_document("brains", saved_public_url, {"url": saved_public_url, "status": "healthy", "source": "restart_reuse"})
728
+ FIREBASE.set_document("tunnels", f"brain_{saved_public_url}", {"kind": "brain", "public_url": saved_public_url, "provider": "ngrok"})
729
+ logger.info("Reusing saved brain public URL after restart: %s", saved_public_url)
730
+ os.environ["KAPO_RESTART_REUSE_PUBLIC_URL"] = "0"
731
+ return saved_public_url
732
+
733
+ configured_public_url = _configured_public_url()
734
+ if configured_public_url:
735
+ _remember_public_url(configured_public_url)
736
+ _report_brain_url(configured_public_url)
737
+ FIREBASE.set_document("brains", configured_public_url, {"url": configured_public_url, "status": "healthy", "source": "configured_public_url"})
738
+ FIREBASE.set_document("tunnels", f"brain_{configured_public_url}", {"kind": "brain", "public_url": configured_public_url, "provider": "configured"})
739
+ logger.info("Using configured brain public URL without starting ngrok: %s", configured_public_url)
740
+ return configured_public_url
741
+
742
+ if not _ngrok_bootstrap_enabled():
743
+ logger.info("Skipping ngrok bootstrap because BRAIN_AUTO_NGROK is disabled")
744
+ return None
745
+
746
+ try:
747
+ authtoken = str(token or os.getenv("NGROK_AUTHTOKEN") or "").strip()
748
+ if not authtoken:
749
+ return None
750
+
751
+ port = int(os.getenv("BRAIN_PORT", "7860"))
752
+ api_url = _find_live_ngrok_api()
753
+ if not api_url:
754
+ api_url = _start_detached_ngrok_agent(authtoken)
755
+ if not api_url:
756
+ logger.warning("Ngrok agent did not expose a local API URL")
757
+ return None
758
+
759
+ public_url = _existing_ngrok_public_url(api_url, port)
760
+ if not public_url:
761
+ public_url = _create_ngrok_tunnel(api_url, port)
762
+ if not public_url:
763
+ return None
764
+
765
+ match = re.search(r"https://[A-Za-z0-9.-]+", public_url)
766
+ if match:
767
+ public_url = match.group(0)
768
+ _remember_ngrok_api_url(api_url)
769
+ _remember_public_url(public_url)
770
+ _report_brain_url(public_url)
771
+ FIREBASE.set_document("brains", public_url, {"url": public_url, "status": "healthy", "source": "ngrok_bootstrap"})
772
+ FIREBASE.set_document("tunnels", f"brain_{public_url}", {"kind": "brain", "public_url": public_url, "provider": "ngrok", "api_url": api_url})
773
+ return public_url
774
+ except Exception:
775
+ logger.exception("Ngrok startup failed")
776
+ return None
777
+
778
+
779
+ def _report_known_public_url() -> str | None:
780
+ public_url = _load_saved_public_url()
781
+ if not public_url:
782
+ return None
783
+ _remember_public_url(public_url)
784
+ _report_brain_url(public_url)
785
+ FIREBASE.set_document("brains", public_url, {"url": public_url, "status": "healthy", "source": "saved_public_url"})
786
+ logger.info("Reported known brain public URL without starting ngrok: %s", public_url)
787
+ return public_url
788
+
789
+
790
+ def _bootstrap_executor_handshake(start_tunnel: bool = False) -> None:
791
+ executor_url = os.getenv("EXECUTOR_URL", "").strip()
792
+ if not executor_url:
793
+ logger.info("Skipping executor handshake: EXECUTOR_URL not configured")
794
+ return
795
+
796
+ settings = _pull_executor_settings()
797
+ _apply_executor_settings(settings)
798
+
799
+ public_url = _report_known_public_url()
800
+ if not public_url and start_tunnel:
801
+ public_url = start_ngrok(os.getenv("NGROK_AUTHTOKEN") or None)
802
+ if public_url:
803
+ logger.info("Brain public URL reported to executor: %s", public_url)
804
+ else:
805
+ logger.info("Brain started without publishing a public URL")
806
+
807
+
808
+ @app.on_event("startup")
809
+ async def startup_event():
810
+ try:
811
+ settings = _pull_executor_settings()
812
+ _apply_executor_settings(settings)
813
+ except Exception:
814
+ logger.exception("Executor settings bootstrap failed")
815
+ try:
816
+ _apply_firebase_runtime_settings()
817
+ except Exception:
818
+ logger.exception("Firebase runtime bootstrap failed")
819
+ try:
820
+ _prepare_runtime_environment()
821
+ except Exception:
822
+ logger.exception("Runtime environment bootstrap failed")
823
+ if str(os.getenv("KAPO_LAZY_MODEL_STARTUP", "1")).strip().lower() not in {"1", "true", "yes", "on"}:
824
+ _load_default_model()
825
+ try:
826
+ if str(os.getenv("KAPO_LAZY_EMBED_STARTUP", "1")).strip().lower() not in {"1", "true", "yes", "on"}:
827
+ _load_embed_model()
828
+ except Exception:
829
+ logger.exception("Embedding model startup failed")
830
+ try:
831
+ start_tunnel = _auto_publish_public_url_on_startup() and not _internal_restart_in_progress()
832
+ _bootstrap_executor_handshake(start_tunnel=start_tunnel)
833
+ except Exception:
834
+ logger.exception("Executor handshake startup failed")
835
+ finally:
836
+ os.environ["KAPO_INTERNAL_RESTART"] = "0"
837
+ FIREBASE.set_document(
838
+ "brains",
839
+ os.getenv("BRAIN_PUBLIC_URL") or os.getenv("KAPO_RUNTIME_ROOT") or "brain_runtime",
840
+ {
841
+ "url": os.getenv("BRAIN_PUBLIC_URL", ""),
842
+ "runtime_root": os.getenv("KAPO_RUNTIME_ROOT", ""),
843
+ "sync_root": os.getenv("KAPO_SYNC_ROOT", ""),
844
+ "model_repo": os.getenv("MODEL_REPO", DEFAULT_MODEL_REPO),
845
+ "model_file": os.getenv("MODEL_FILE", DEFAULT_MODEL_FILE),
846
+ "model_profile_id": os.getenv("MODEL_PROFILE_ID", DEFAULT_MODEL_PROFILE_ID),
847
+ "roles": os.getenv("BRAIN_ROLES", "supervisor,chat,coding,planner,arabic,fallback"),
848
+ "languages": os.getenv("BRAIN_LANGUAGES", "ar,en"),
849
+ "status": "starting",
850
+ },
851
+ )
852
+
853
+
854
+ class ModelLoadRequest(BaseModel):
855
+ repo_id: str
856
+ filename: str
857
+ hf_token: str | None = None
858
+
859
+
860
+ class ConnectionInit(BaseModel):
861
+ executor_url: str
862
+ ngrok_token: str | None = None
863
+
864
+
865
+ class PublishUrlRequest(BaseModel):
866
+ ngrok_token: str | None = None
867
+ public_url: str | None = None
868
+ start_tunnel: bool = True
869
+
870
+
871
+ class RestartRequest(BaseModel):
872
+ delay_sec: float = 1.0
873
+
874
+
875
+ class FileWriteRequest(BaseModel):
876
+ path: str
877
+ content: str = ""
878
+ overwrite: bool = True
879
+
880
+
881
+ class FileDeleteRequest(BaseModel):
882
+ path: str
883
+ recursive: bool = False
884
+
885
+
886
+ class FileMkdirRequest(BaseModel):
887
+ path: str
888
+
889
+
890
+ class ChatRequest(BaseModel):
891
+ request_id: str
892
+ user_input: str
893
+ context: dict[str, Any] = {}
894
+ history: list[dict[str, str]] = []
895
+ auto_execute: bool = True
896
+
897
+
898
+ def _contains_arabic(text: str) -> bool:
899
+ return bool(re.search(r"[\u0600-\u06FF]", text or ""))
900
+
901
+
902
+ def _detect_language(text: str) -> str:
903
+ return "ar" if _contains_arabic(text) else "en"
904
+
905
+
906
+ def _is_task_request(text: str) -> bool:
907
+ lower = (text or "").strip().lower()
908
+ task_words = [
909
+ "build", "fix", "debug", "create project", "generate project", "implement", "refactor",
910
+ "run", "execute", "install", "modify", "edit", "update", "write code", "make app",
911
+ "انشئ", "أنشئ", "اعمل", "نفذ", "شغل", "اصلح", "أصلح", "عدّل", "عدل", "ابني", "كوّن مشروع",
912
+ ]
913
+ return any(word in lower for word in task_words)
914
+
915
+
916
+ def _is_research_request(text: str) -> bool:
917
+ lower = (text or "").strip().lower()
918
+ research_words = [
919
+ "search", "research", "look up", "find out", "web", "browse",
920
+ "ابحث", "ابحث عن", "دور", "فتش", "معلومة عن", "معلومات عن",
921
+ ]
922
+ return any(word in lower for word in research_words)
923
+
924
+
925
+ def _is_knowledge_request(text: str, context: dict[str, Any] | None = None) -> bool:
926
+ context = context or {}
927
+ if bool(context.get("use_executor_knowledge")):
928
+ return True
929
+ lower = (text or "").strip().lower()
930
+ knowledge_words = [
931
+ "remember", "memory", "knowledge", "docs", "documentation", "project structure", "architecture",
932
+ "تذكر", "الذاكرة", "المعرفة", "الوثائق", "الدليل", "بنية المشروع", "هيكل المشروع", "معمارية",
933
+ ]
934
+ return any(word in lower for word in knowledge_words)
935
+
936
+
937
+ def _prune_history(history: list[dict[str, str]], keep_last: int = 6) -> list[dict[str, str]]:
938
+ if len(history) <= keep_last:
939
+ return history
940
+ return history[-keep_last:]
941
+
942
+
943
+ def _retrieve_knowledge(query: str, top_k: int = 4) -> list[dict[str, Any]]:
944
+ executor_url = os.getenv("EXECUTOR_URL", "").strip().rstrip("/")
945
+ expanded_query = _expand_project_query(query)
946
+ if _executor_roundtrip_allowed("BRAIN_REMOTE_KNOWLEDGE_ENABLED", default=True):
947
+ try:
948
+ response = requests.get(
949
+ f"{executor_url}/rag/search",
950
+ params={"query": expanded_query, "top_k": top_k},
951
+ headers=_brain_headers(),
952
+ timeout=(
953
+ _executor_connect_timeout(),
954
+ _executor_read_timeout("BRAIN_REMOTE_KNOWLEDGE_TIMEOUT_SEC", 6.0),
955
+ ),
956
+ )
957
+ if response.status_code == 200:
958
+ payload = response.json()
959
+ results = payload.get("results", [])
960
+ if isinstance(results, list):
961
+ return results
962
+ except requests.exceptions.ReadTimeout:
963
+ logger.info("Executor knowledge retrieval timed out; continuing without remote knowledge")
964
+ except Exception:
965
+ logger.warning("Executor knowledge retrieval failed", exc_info=True)
966
+ if _remote_brain_only() or not _feature_enabled("BRAIN_LOCAL_RAG_FALLBACK_ENABLED", default=False):
967
+ return []
968
+ try:
969
+ from rag.retriever import retrieve
970
+
971
+ return retrieve(expanded_query, top_k=top_k)
972
+ except Exception:
973
+ logger.warning("Knowledge retrieval failed", exc_info=True)
974
+ return []
975
+
976
+
977
+ def _search_web(query: str) -> list[dict[str, Any]]:
978
+ executor_url = os.getenv("EXECUTOR_URL", "").strip().rstrip("/")
979
+ if not _executor_roundtrip_allowed("BRAIN_REMOTE_WEB_SEARCH_ENABLED", default=True):
980
+ return []
981
+ try:
982
+ response = requests.post(
983
+ f"{executor_url}/tools/search",
984
+ json={"query": query, "num_results": 5},
985
+ headers=_brain_headers(),
986
+ timeout=(
987
+ _executor_connect_timeout(),
988
+ _executor_read_timeout("BRAIN_REMOTE_WEB_SEARCH_TIMEOUT_SEC", 8.0),
989
+ ),
990
+ )
991
+ if response.status_code == 200:
992
+ payload = response.json()
993
+ results = payload.get("results", [])
994
+ return results if isinstance(results, list) else []
995
+ except Exception:
996
+ logger.warning("Web search failed", exc_info=True)
997
+ return []
998
+
999
+
1000
+ def _format_context_blocks(knowledge: list[dict[str, Any]], web_results: list[dict[str, Any]]) -> str:
1001
+ blocks: list[str] = []
1002
+ if knowledge:
1003
+ lines = []
1004
+ for item in knowledge[:4]:
1005
+ source = item.get("source", "knowledge")
1006
+ content = item.get("content", "") or item.get("text", "")
1007
+ lines.append(f"- [{source}] {content[:500]}")
1008
+ blocks.append("Knowledge:\n" + "\n".join(lines))
1009
+ if web_results:
1010
+ lines = []
1011
+ for item in web_results[:5]:
1012
+ lines.append(f"- {item.get('title', '')}: {item.get('snippet', '')}")
1013
+ blocks.append("Web:\n" + "\n".join(lines))
1014
+ return "\n\n".join(blocks).strip()
1015
+
1016
+
1017
+ def _project_context_tags(text: str) -> list[str]:
1018
+ source = str(text or "")
1019
+ lowered = source.lower()
1020
+ tags: list[str] = []
1021
+ tag_rules = [
1022
+ ("brain_runtime", ["العقل", "brain", "model", "موديل", "النموذج"]),
1023
+ ("executor_runtime", ["الوكيل التنفيذي", "executor", "agent"]),
1024
+ ("tunnel_runtime", ["النفق", "tunnel", "ngrok", "cloudflare"]),
1025
+ ("url_routing", ["الرابط", "url", "endpoint", "لينك"]),
1026
+ ("restart_sync", ["restart", "ريستارت", "إعادة تشغيل", "اعادة تشغيل", "sync", "مزامنة"]),
1027
+ ("knowledge_memory", ["memory", "ذاكرة", "معرفة", "knowledge", "rag", "طبقات", "embedding", "embeddings"]),
1028
+ ("kaggle_runtime", ["kaggle", "كاجل"]),
1029
+ ]
1030
+ for tag, markers in tag_rules:
1031
+ if any(marker in source or marker in lowered for marker in markers):
1032
+ tags.append(tag)
1033
+ return tags
1034
+
1035
+
1036
+ def _expand_project_query(query: str) -> str:
1037
+ tags = _project_context_tags(query)
1038
+ additions: list[str] = []
1039
+ if "tunnel_runtime" in tags:
1040
+ additions.append("ngrok tunnel public url reverse proxy runtime restart")
1041
+ if "restart_sync" in tags:
1042
+ additions.append("system restart sync uvicorn process reuse public url")
1043
+ if "executor_runtime" in tags:
1044
+ additions.append("executor share settings control plane local machine")
1045
+ if "knowledge_memory" in tags:
1046
+ additions.append("knowledge layers rag embeddings preferences profile")
1047
+ if "brain_runtime" in tags:
1048
+ additions.append("brain server kaggle model startup runtime")
1049
+ return query if not additions else f"{query}\nContext expansion: {' | '.join(additions)}"
1050
+
1051
+
1052
+ def _fetch_style_profile() -> dict[str, Any]:
1053
+ firebase_profile = FIREBASE.get_document("profiles", "style", ttl_sec=30.0)
1054
+ if firebase_profile:
1055
+ return firebase_profile
1056
+ executor_url = os.getenv("EXECUTOR_URL", "").strip().rstrip("/")
1057
+ if not executor_url or not _executor_roundtrip_allowed("BRAIN_REMOTE_STYLE_PROFILE_ENABLED", default=False):
1058
+ return {}
1059
+ try:
1060
+ response = requests.get(
1061
+ f"{executor_url}/preferences/profile",
1062
+ headers=_brain_headers(),
1063
+ timeout=(
1064
+ _executor_connect_timeout(),
1065
+ _executor_read_timeout("BRAIN_REMOTE_STYLE_PROFILE_TIMEOUT_SEC", 2.5),
1066
+ ),
1067
+ )
1068
+ if response.status_code != 200:
1069
+ return {}
1070
+ payload = response.json().get("profile", {})
1071
+ return payload if isinstance(payload, dict) else {}
1072
+ except requests.exceptions.ReadTimeout:
1073
+ logger.info("Style profile load timed out; continuing without remote style profile")
1074
+ return {}
1075
+ except Exception:
1076
+ logger.warning("Failed to load style profile", exc_info=True)
1077
+ return {}
1078
+
1079
+
1080
+ def _render_style_profile_context(profile: dict[str, Any]) -> str:
1081
+ if not profile:
1082
+ return ""
1083
+ preferences = profile.get("preferences", []) or []
1084
+ examples = profile.get("examples", []) or []
1085
+ lexical_signals = profile.get("lexical_signals", []) or []
1086
+ style_markers = profile.get("style_markers", {}) or {}
1087
+ persona_summary = str(profile.get("persona_summary") or "").strip()
1088
+ response_contract = str(profile.get("response_contract") or "").strip()
1089
+ lines: list[str] = []
1090
+ if persona_summary:
1091
+ lines.append(f"User persona summary: {persona_summary}")
1092
+ if response_contract:
1093
+ lines.append(f"Response contract: {response_contract}")
1094
+ if preferences:
1095
+ lines.append("User Style Preferences:")
1096
+ for item in preferences[:10]:
1097
+ lines.append(f"- {item}")
1098
+ if lexical_signals:
1099
+ lines.append("User Lexical Signals:")
1100
+ for item in lexical_signals[:10]:
1101
+ lines.append(f"- {item}")
1102
+ if style_markers:
1103
+ lines.append("Style Markers:")
1104
+ for key, value in sorted(style_markers.items()):
1105
+ lines.append(f"- {key}: {value}")
1106
+ if examples:
1107
+ lines.append("Recent User Style Examples:")
1108
+ for sample in examples[-3:]:
1109
+ user_text = str(sample.get("user_input") or "").strip()
1110
+ assistant_text = str(sample.get("assistant_reply") or "").strip()
1111
+ if user_text:
1112
+ lines.append(f"- User: {user_text}")
1113
+ if assistant_text:
1114
+ lines.append(f" Assistant: {assistant_text}")
1115
+ return "\n".join(lines).strip()
1116
+
1117
+
1118
+ def _project_domain_context(user_input: str, context: dict[str, Any] | None = None) -> str:
1119
+ tags = _project_context_tags(user_input)
1120
+ if not tags:
1121
+ return ""
1122
+ lines = [
1123
+ "Project Domain Glossary:",
1124
+ "- In this project, terms like العقل, الوكيل التنفيذي, النفق, الرابط, الريستارت, المزامنة, الذاكرة, والطبقات usually refer to software runtime and operations concepts.",
1125
+ "- Treat النفق as ngrok or cloudflare tunnel unless the user explicitly asks for a literal/civil meaning.",
1126
+ "- Treat الرابط as public URL, endpoint, or routing target when the surrounding context mentions deployment, Kaggle, restart, or sync.",
1127
+ "- Treat العقل as the remote Brain service/model runtime, and الوكيل التنفيذي as the local executor/control plane on the user's device.",
1128
+ ]
1129
+ if "restart_sync" in tags:
1130
+ lines.append("- Restart means process/service restart; preserving the same public URL matters more than creating a fresh tunnel.")
1131
+ if "knowledge_memory" in tags:
1132
+ lines.append("- Knowledge, embeddings, layers, and memory refer to the RAG and memory system inside this project.")
1133
+ if "kaggle_runtime" in tags:
1134
+ lines.append("- Kaggle here is the remote runtime hosting the Brain service.")
1135
+ role_name = str((context or {}).get("role_name") or "").strip()
1136
+ if role_name:
1137
+ lines.append(f"- Current assigned role: {role_name}.")
1138
+ return "\n".join(lines)
1139
+
1140
+
1141
+ def _firebase_runtime_context(role_name: str, language: str) -> str:
1142
+ snapshot = _firebase_runtime_snapshot()
1143
+ lines: list[str] = []
1144
+ roles = snapshot.get("roles") or []
1145
+ if roles:
1146
+ enabled_roles = [
1147
+ str(item.get("name") or item.get("id") or "").strip()
1148
+ for item in roles
1149
+ if str(item.get("enabled", True)).strip().lower() not in {"0", "false", "no", "off"}
1150
+ ]
1151
+ enabled_roles = [item for item in enabled_roles if item]
1152
+ if enabled_roles:
1153
+ lines.append("Live roles from Firestore: " + ", ".join(enabled_roles[:12]))
1154
+ models = snapshot.get("models") or []
1155
+ if models:
1156
+ preferred = [
1157
+ item for item in models
1158
+ if str(item.get("enabled", True)).strip().lower() not in {"0", "false", "no", "off"}
1159
+ ]
1160
+ if preferred:
1161
+ labels = [str(item.get("label") or item.get("id") or "").strip() for item in preferred[:5]]
1162
+ labels = [item for item in labels if item]
1163
+ if labels:
1164
+ lines.append("Live model profiles: " + ", ".join(labels))
1165
+ platforms = snapshot.get("platforms") or []
1166
+ if platforms:
1167
+ names = [str(item.get("name") or "").strip() for item in platforms[:6] if str(item.get("name") or "").strip()]
1168
+ if names:
1169
+ lines.append("Live platforms: " + ", ".join(names))
1170
+ prompt_body = _firebase_prompt_body(role_name or "chat", language) or _firebase_prompt_body(role_name or "chat", "en")
1171
+ if prompt_body:
1172
+ lines.append(f"Live Firestore prompt for role '{role_name or 'chat'}': {prompt_body}")
1173
+ return "\n".join(lines).strip()
1174
+
1175
+
1176
+ def _append_runtime_instructions(context_block: str, context: dict[str, Any]) -> str:
1177
+ instructions = str((context or {}).get("system_instructions") or "").strip()
1178
+ role_name = str((context or {}).get("role_name") or "").strip()
1179
+ user_input = str((context or {}).get("user_input") or "").strip()
1180
+ language = _detect_language(user_input)
1181
+ style_profile = _render_style_profile_context(_fetch_style_profile())
1182
+ domain_context = _project_domain_context(user_input, context)
1183
+ firebase_context = _firebase_runtime_context(role_name, language)
1184
+ if not instructions and not role_name and not style_profile and not domain_context and not firebase_context:
1185
+ return context_block
1186
+ extra = []
1187
+ if role_name:
1188
+ extra.append(f"Assigned role: {role_name}")
1189
+ if instructions:
1190
+ extra.append(instructions)
1191
+ if firebase_context:
1192
+ extra.append(firebase_context)
1193
+ if style_profile:
1194
+ extra.append(style_profile)
1195
+ if domain_context:
1196
+ extra.append(domain_context)
1197
+ extra_block = "Runtime Instructions:\n" + "\n".join(extra)
1198
+ return (context_block + "\n\n" + extra_block).strip() if context_block else extra_block
1199
+
1200
+
1201
+ def _extract_exact_reply_instruction(user_input: str) -> str:
1202
+ text = (user_input or "").strip()
1203
+ patterns = [
1204
+ r'(?is)reply\s+with\s+exactly\s+[:"]?\s*(.+?)\s*[".]?$',
1205
+ r'(?is)respond\s+with\s+exactly\s+[:"]?\s*(.+?)\s*[".]?$',
1206
+ r"(?is)قل\s+فقط[::]?\s*(.+?)\s*$",
1207
+ r"(?is)اكتب\s+فقط[::]?\s*(.+?)\s*$",
1208
+ ]
1209
+ for pattern in patterns:
1210
+ match = re.search(pattern, text)
1211
+ if match:
1212
+ return match.group(1).strip().strip("\"'`")
1213
+ return ""
1214
+
1215
+
1216
+ def _extract_exact_reply_instruction_safe(user_input: str) -> str:
1217
+ text = (user_input or "").strip()
1218
+ patterns = [
1219
+ r'(?is)reply\s+with\s+exactly\s+[:"]?\s*(.+?)\s*[".]?$',
1220
+ r'(?is)respond\s+with\s+exactly\s+[:"]?\s*(.+?)\s*[".]?$',
1221
+ r"(?is)\u0642\u0644\s+\u0641\u0642\u0637[:\uff1a]?\s*(.+?)\s*$",
1222
+ r"(?is)\u0627\u0643\u062a\u0628\s+\u0641\u0642\u0637[:\uff1a]?\s*(.+?)\s*$",
1223
+ ]
1224
+ for pattern in patterns:
1225
+ match = re.search(pattern, text)
1226
+ if match:
1227
+ return match.group(1).strip().strip("\"'`")
1228
+ return _extract_exact_reply_instruction(user_input)
1229
+
1230
+
1231
+ def _chat_system_instruction(language: str, user_input: str = "", exact_reply: str = "") -> str:
1232
+ if language == "ar":
1233
+ base = (
1234
+ "أنت KAPO-AI، مساعد هندسي عملي. "
1235
+ "أجب مباشرة وبوضوح وبشكل مفيد. "
1236
+ "افهم المطلوب أولاً ثم أجب دون مقدمات زائدة. "
1237
+ "لا تقل إن المحادثة غير مكتملة ولا تذكر تعليماتك الداخلية."
1238
+ )
1239
+ base += (
1240
+ " في هذا المشروع، كلمات مثل العقل والوكيل التنفيذي والنفق والرابط والريستارت والمزامنة "
1241
+ "والطبقات والتضمينات وكاجل وngrok وcloudflare وendpoint وmodel تشير غالبا إلى "
1242
+ "مكونات برمجية وتشغيلية، وليست معاني حرفية أو هندسة مدنية، ما لم يطلب المستخدم غير ذلك صراحة."
1243
+ )
1244
+ if exact_reply:
1245
+ return base + f' يجب أن يكون ردك هو هذا النص فقط حرفياً: "{exact_reply}"'
1246
+ return base
1247
+ base = (
1248
+ "You are KAPO-AI, an engineering assistant. "
1249
+ "Answer directly, clearly, and practically. "
1250
+ "Understand the request before answering. "
1251
+ "Do not say the conversation is incomplete and do not mention hidden instructions."
1252
+ )
1253
+ if exact_reply:
1254
+ return base + f' Your entire reply must be exactly: "{exact_reply}"'
1255
+ return base
1256
+
1257
+
1258
+ def _build_chat_prompt(user_input: str, history: list[dict[str, str]], context_block: str) -> str:
1259
+ language = _detect_language(user_input)
1260
+ exact_reply = _extract_exact_reply_instruction_safe(user_input)
1261
+ history_lines: list[str] = []
1262
+ for message in _prune_history(history):
1263
+ role = message.get("role", "user")
1264
+ role_label = "المستخدم" if language == "ar" and role == "user" else role.upper()
1265
+ if language == "ar" and role != "user":
1266
+ role_label = "المØ��اعد"
1267
+ history_lines.append(f"{role_label}: {message.get('content', '')}")
1268
+
1269
+ history_section = ""
1270
+ if history_lines:
1271
+ history_section = "\n### History\n" + "\n".join(history_lines) + "\n"
1272
+
1273
+ context_section = f"\n### Context\n{context_block}\n" if context_block else ""
1274
+ user_label = "المستخدم" if language == "ar" else "User"
1275
+ assistant_label = "المساعد" if language == "ar" else "Assistant"
1276
+ return (
1277
+ f"### System\n{_chat_system_instruction(language, user_input, exact_reply)}\n"
1278
+ f"{history_section}"
1279
+ f"{context_section}"
1280
+ f"### Instruction\n"
1281
+ f"{user_label}: {user_input}\n"
1282
+ f"{assistant_label}:"
1283
+ )
1284
+
1285
+
1286
+ def _response_looks_bad(text: str, language: str) -> bool:
1287
+ cleaned = (text or "").strip()
1288
+ if not cleaned:
1289
+ return True
1290
+ markers = [
1291
+ "the assistant is not sure",
1292
+ "conversation seems incomplete",
1293
+ "provide more information",
1294
+ "unless otherwise noted",
1295
+ "as an ai model developed by",
1296
+ "developed by ibm",
1297
+ "tensorflow library",
1298
+ "dataset of 1024",
1299
+ ]
1300
+ if any(marker in cleaned.lower() for marker in markers):
1301
+ return True
1302
+ if language == "ar":
1303
+ arabic_chars = len(re.findall(r"[\u0600-\u06FF]", cleaned))
1304
+ latin_chars = len(re.findall(r"[A-Za-z]", cleaned))
1305
+ if arabic_chars < 8 and latin_chars > max(12, arabic_chars * 2):
1306
+ return True
1307
+ return False
1308
+
1309
+
1310
+ def _fallback_response(user_input: str) -> str:
1311
+ if _detect_language(user_input) == "ar":
1312
+ return "فهمت رسالتك، لكن الرد المولد لم يكن صالحاً للاستخدام. أعد صياغة الطلب بشكل أكثر تحديداً."
1313
+ return "I understood your message, but the generated reply was not usable. Please rephrase the request more specifically."
1314
+
1315
+
1316
+ def _project_specific_fast_reply(user_input: str) -> str:
1317
+ text = (user_input or "").strip()
1318
+ lower = text.lower()
1319
+ if any(token in text for token in ("ايه اللي اتصلح", "إيه اللي اتصلح", "هو ايه اللي اتصلح", "هو إيه اللي اتصلح")) and any(
1320
+ token in text or token in lower for token in ("النفق", "الرابط", "الريستارت", "restart")
1321
+ ):
1322
+ return (
1323
+ "الذي اتصلح هو أن الريستارت الداخلي بقى يعيد تشغيل خدمة العقل نفسها من غير ما يكسر النفق أو يغيّر الرابط العام. "
1324
+ "ولو ظهر توقف قصير أثناء الإقلاع فهذا يكون من رجوع الخدمة، لا من إنشاء نفق جديد."
1325
+ )
1326
+ if "النفق" in text and any(token in text for token in ("إصلاح", "اصلاح", "الذي تم", "اتصلح", "تم إصلاح", "تم اصلاح")):
1327
+ return (
1328
+ "تم فصل دورة حياة النفق عن دورة حياة خدمة العقل، فأصبح ngrok يعمل كعامل مستقل عن عملية Uvicorn. "
1329
+ "وبالتالي يحتفظ /system/restart بنفس الرابط العام بدل إنشاء رابط جديد، وقد يظهر ERR_NGROK_8012 مؤقتًا فقط أثناء الإقلاع."
1330
+ )
1331
+ if "نفس الرابط" in text and ("ريستارت" in text or "restart" in lower):
1332
+ return (
1333
+ "الهدف هنا أن تعيد الخدمة الإقلاع داخليًا مع الإبقاء على نفس الـ public URL. "
1334
+ "لذلك يبقى tunnel حيًا، بينما تعود خدمة localhost:7860 للعمل بعد ثوانٍ قليلة على نفس الرابط."
1335
+ )
1336
+ return ""
1337
+
1338
+
1339
+ def _generate_response(user_input: str, history: list[dict[str, str]], context_block: str) -> str:
1340
+ language = _detect_language(user_input)
1341
+ exact_reply = _extract_exact_reply_instruction_safe(user_input)
1342
+ if exact_reply:
1343
+ return exact_reply
1344
+ fast_reply = _project_specific_fast_reply(user_input)
1345
+ if fast_reply:
1346
+ return fast_reply
1347
+ if MODEL is None:
1348
+ if language == "ar":
1349
+ return "الخدمة تعمل لكن توليد الرد الحر غير متاح الآن لأن النموذج غير محمل."
1350
+ return "The Brain is online, but natural chat generation is unavailable because the model is not loaded."
1351
+
1352
+ prompt = _build_chat_prompt(user_input, history, context_block)
1353
+
1354
+ try:
1355
+ max_tokens = 80 if language == "ar" else 96
1356
+ output = MODEL(
1357
+ prompt,
1358
+ max_tokens=max_tokens,
1359
+ temperature=0.1,
1360
+ top_p=0.85,
1361
+ stop=["\nUser:", "\nUSER:", "\nالمستخدم:", "\n###", "<|EOT|>"],
1362
+ )
1363
+ text = output["choices"][0]["text"].strip()
1364
+ if _response_looks_bad(text, language):
1365
+ return _fallback_response(user_input)
1366
+ return text or ("تم استلام رسالتك." if language == "ar" else "I received your message.")
1367
+ except Exception:
1368
+ logger.exception("Model generation failed")
1369
+ if language == "ar":
1370
+ return "فهمت طلبك، لكن فشل توليد الرد النصي."
1371
+ return "I understood your request, but text generation failed."
1372
+
1373
+
1374
+ def _store_chat_trace(request_id: str, payload: dict[str, Any]) -> None:
1375
+ executor_url = os.getenv("EXECUTOR_URL", "").strip().rstrip("/")
1376
+ if not _executor_roundtrip_allowed("BRAIN_REMOTE_TRACE_STORE_ENABLED", default=True):
1377
+ return
1378
+ try:
1379
+ requests.post(
1380
+ f"{executor_url}/memory/store",
1381
+ json={"request_id": request_id, "payload": payload},
1382
+ headers=_brain_headers(),
1383
+ timeout=(
1384
+ _executor_connect_timeout(),
1385
+ _executor_read_timeout("BRAIN_REMOTE_TRACE_STORE_TIMEOUT_SEC", 2.5),
1386
+ ),
1387
+ )
1388
+ except requests.exceptions.ReadTimeout:
1389
+ logger.info("Chat trace store timed out; continuing")
1390
+ except Exception:
1391
+ logger.warning("Failed to store chat trace on executor", exc_info=True)
1392
+
1393
+
1394
+ def _ingest_chat_knowledge(request_id: str, user_input: str, reply: str) -> None:
1395
+ if len(reply or "") < 180:
1396
+ return
1397
+ executor_url = os.getenv("EXECUTOR_URL", "").strip().rstrip("/")
1398
+ if not _executor_roundtrip_allowed("BRAIN_REMOTE_AUTO_INGEST_ENABLED", default=False):
1399
+ return
1400
+ payload = {
1401
+ "request_id": request_id,
1402
+ "payload": {
1403
+ "source": "auto_chat",
1404
+ "content": f"User: {user_input}\nAssistant: {reply}",
1405
+ },
1406
+ }
1407
+ try:
1408
+ requests.post(
1409
+ f"{executor_url}/rag/ingest",
1410
+ json=payload,
1411
+ headers=_brain_headers(),
1412
+ timeout=(
1413
+ _executor_connect_timeout(),
1414
+ _executor_read_timeout("BRAIN_REMOTE_AUTO_INGEST_TIMEOUT_SEC", 3.0),
1415
+ ),
1416
+ )
1417
+ except requests.exceptions.ReadTimeout:
1418
+ logger.info("Auto-ingest chat knowledge timed out; continuing")
1419
+ except Exception:
1420
+ logger.warning("Failed to auto-ingest chat knowledge", exc_info=True)
1421
+
1422
+
1423
+ def _learn_user_style(request_id: str, user_input: str, reply: str, context: dict[str, Any]) -> None:
1424
+ executor_url = os.getenv("EXECUTOR_URL", "").strip().rstrip("/")
1425
+ if not executor_url or not _executor_roundtrip_allowed("BRAIN_REMOTE_STYLE_PROFILE_ENABLED", default=False):
1426
+ return
1427
+ try:
1428
+ requests.post(
1429
+ f"{executor_url}/preferences/learn",
1430
+ json={
1431
+ "request_id": request_id,
1432
+ "user_input": user_input,
1433
+ "assistant_reply": reply,
1434
+ "context": context or {},
1435
+ },
1436
+ headers=_brain_headers(),
1437
+ timeout=(
1438
+ _executor_connect_timeout(),
1439
+ _executor_read_timeout("BRAIN_REMOTE_TRACE_STORE_TIMEOUT_SEC", 2.5),
1440
+ ),
1441
+ )
1442
+ except requests.exceptions.ReadTimeout:
1443
+ logger.info("Style learning timed out; continuing")
1444
+ except Exception:
1445
+ logger.warning("Failed to learn user style on executor", exc_info=True)
1446
+
1447
+
1448
+ def _dispatch_background(task, *args) -> None:
1449
+ try:
1450
+ threading.Thread(target=task, args=args, daemon=True).start()
1451
+ except Exception:
1452
+ logger.warning("Background task dispatch failed", exc_info=True)
1453
+
1454
+
1455
+ def _restart_process(delay_sec: float = 1.0) -> None:
1456
+ if _is_hf_space_runtime():
1457
+ logger.info("Skipping in-process restart on Hugging Face Space runtime")
1458
+ return
1459
+ def _run() -> None:
1460
+ time.sleep(max(0.2, float(delay_sec)))
1461
+ target_root = _sync_target_root()
1462
+ os.chdir(target_root)
1463
+ os.environ["KAPO_INTERNAL_RESTART"] = "1"
1464
+ if _reuse_public_url_on_restart():
1465
+ current_public_url = _load_saved_public_url()
1466
+ if current_public_url:
1467
+ _remember_public_url(current_public_url)
1468
+ os.environ["KAPO_RESTART_REUSE_PUBLIC_URL"] = "1"
1469
+ port = str(os.getenv("BRAIN_PORT", "7860") or "7860")
1470
+ app_module = str(os.getenv("KAPO_UVICORN_APP") or "").strip()
1471
+ if not app_module:
1472
+ if (Path(target_root) / "brain_server" / "api" / "main.py").exists():
1473
+ app_module = "brain_server.api.main:app"
1474
+ elif (Path(target_root) / "api" / "main.py").exists():
1475
+ app_module = "api.main:app"
1476
+ else:
1477
+ app_module = "brain_server.api.main:app"
1478
+ os.execv(
1479
+ sys.executable,
1480
+ [
1481
+ sys.executable,
1482
+ "-m",
1483
+ "uvicorn",
1484
+ app_module,
1485
+ "--host",
1486
+ "0.0.0.0",
1487
+ "--port",
1488
+ port,
1489
+ ],
1490
+ )
1491
+
1492
+ threading.Thread(target=_run, daemon=True).start()
1493
+
1494
+
1495
+ @app.get("/")
1496
+ async def root():
1497
+ return {"status": "ok", "service": "brain_server", "docs": "/docs", "health": "/health"}
1498
+
1499
+
1500
+ @app.get("/runtime/firebase")
1501
+ async def runtime_firebase_snapshot():
1502
+ try:
1503
+ return {"status": "ok", "firebase": _json_safe(_firebase_runtime_snapshot())}
1504
+ except Exception as exc:
1505
+ logger.warning("Failed to build firebase runtime snapshot", exc_info=True)
1506
+ return {"status": "degraded", "firebase": {"platforms": [], "models": [], "prompts": [], "roles": []}, "detail": str(exc)}
1507
+
1508
+
1509
+ @app.get("/runtime/errors")
1510
+ async def runtime_errors(limit: int = 50, level: str = "WARNING"):
1511
+ normalized = str(level or "WARNING").strip().upper()
1512
+ allowed = {"DEBUG": 10, "INFO": 20, "WARNING": 30, "ERROR": 40, "CRITICAL": 50}
1513
+ threshold = allowed.get(normalized, 30)
1514
+ items = [item for item in list(RUNTIME_LOG_BUFFER) if allowed.get(str(item.get("level") or "").upper(), 0) >= threshold]
1515
+ return {"status": "ok", "count": len(items[-limit:]), "items": items[-limit:]}
1516
+
1517
+
1518
+ @app.get("/model/status")
1519
+ async def model_status():
1520
+ return {
1521
+ "loaded": MODEL is not None,
1522
+ "error": MODEL_ERROR,
1523
+ "repo_id": MODEL_META.get("repo_id"),
1524
+ "filename": MODEL_META.get("filename"),
1525
+ }
1526
+
1527
+
1528
+ @app.post("/model/load")
1529
+ async def model_load(req: ModelLoadRequest):
1530
+ ensure_model_loaded(req.repo_id, req.filename, hf_token=req.hf_token)
1531
+ return await model_status()
1532
+
1533
+
1534
+ @app.post("/model/hotswap")
1535
+ async def model_hotswap(req: ModelLoadRequest):
1536
+ global MODEL
1537
+ if MODEL is not None:
1538
+ del MODEL
1539
+ gc.collect()
1540
+ MODEL = None
1541
+ ensure_model_loaded(req.repo_id, req.filename, hf_token=req.hf_token)
1542
+ return await model_status()
1543
+
1544
+
1545
+ @app.post("/embeddings")
1546
+ async def embeddings(payload: dict[str, Any]):
1547
+ if EMBED_MODEL is None:
1548
+ _load_embed_model()
1549
+ texts = payload.get("texts") or []
1550
+ if not texts:
1551
+ return {"embeddings": []}
1552
+ return {"embeddings": EMBED_MODEL.encode(texts).tolist()}
1553
+
1554
+
1555
+ @app.post("/chat")
1556
+ async def chat(req: ChatRequest):
1557
+ try:
1558
+ exact_reply = _extract_exact_reply_instruction_safe(req.user_input)
1559
+ if exact_reply:
1560
+ runtime_context = {**(req.context or {}), "user_input": req.user_input}
1561
+ trace_payload = {
1562
+ "mode": "chat",
1563
+ "user_input": req.user_input,
1564
+ "reply": exact_reply,
1565
+ "plan": None,
1566
+ "execution": None,
1567
+ "knowledge": [],
1568
+ "web_results": [],
1569
+ "context": runtime_context,
1570
+ }
1571
+ memory = MemoryAgent()
1572
+ memory.write_short_term(req.request_id, trace_payload)
1573
+ return {
1574
+ "status": "ok",
1575
+ "mode": "chat",
1576
+ "reply": exact_reply,
1577
+ "plan": None,
1578
+ "rationale": None,
1579
+ "execution": None,
1580
+ "knowledge": [],
1581
+ "web_results": [],
1582
+ "timestamp": time.time(),
1583
+ }
1584
+
1585
+ planner = PlannerAgent()
1586
+ reasoning = ReasoningAgent()
1587
+ memory = MemoryAgent()
1588
+
1589
+ mode = "task" if _is_task_request(req.user_input) else "chat"
1590
+ runtime_context = {**(req.context or {}), "user_input": req.user_input}
1591
+ knowledge = _retrieve_knowledge(req.user_input, top_k=4) if _is_knowledge_request(req.user_input, req.context) else []
1592
+ web_results = _search_web(req.user_input) if _is_research_request(req.user_input) else []
1593
+ context_block = _append_runtime_instructions(_format_context_blocks(knowledge, web_results), runtime_context)
1594
+ response_text = ""
1595
+ plan_steps = None
1596
+ execution = None
1597
+ rationale = None
1598
+
1599
+ if mode == "task":
1600
+ plan_steps = planner.run(req.user_input, req.context)
1601
+ rationale = reasoning.run(req.user_input, plan_steps)
1602
+ response_text = (
1603
+ "سأتعامل مع هذه الرسالة كطلب تنفيذ، وقمت ببناء خطة مبدئية وسأبدأ التنفيذ تلقائياً."
1604
+ if _contains_arabic(req.user_input)
1605
+ else "I treated this as an execution request, built a plan, and started automatic execution."
1606
+ )
1607
+ if req.auto_execute:
1608
+ from api.routes_execute import ExecuteRequest, execute as execute_route
1609
+
1610
+ execution = await execute_route(
1611
+ ExecuteRequest(
1612
+ request_id=req.request_id,
1613
+ plan={"steps": plan_steps},
1614
+ executor_url=os.getenv("EXECUTOR_URL", "").strip() or None,
1615
+ )
1616
+ )
1617
+ if execution.get("report", {}).get("success"):
1618
+ response_text += (
1619
+ "\n\nتم التنفيذ بنجاح مبدئياً."
1620
+ if _contains_arabic(req.user_input)
1621
+ else "\n\nExecution completed successfully."
1622
+ )
1623
+ else:
1624
+ response_text += (
1625
+ "\n\nتمت المحاولة لكن توجد مخرجات تحتاج مراجعة."
1626
+ if _contains_arabic(req.user_input)
1627
+ else "\n\nExecution ran, but the result still needs review."
1628
+ )
1629
+ else:
1630
+ response_text = _generate_response(req.user_input, req.history, context_block)
1631
+
1632
+ trace_payload = {
1633
+ "mode": mode,
1634
+ "user_input": req.user_input,
1635
+ "reply": response_text,
1636
+ "plan": plan_steps,
1637
+ "execution": execution,
1638
+ "knowledge": knowledge,
1639
+ "web_results": web_results,
1640
+ "context": runtime_context,
1641
+ }
1642
+ memory.write_short_term(req.request_id, trace_payload)
1643
+ _dispatch_background(_store_chat_trace, req.request_id, trace_payload)
1644
+ _dispatch_background(_ingest_chat_knowledge, req.request_id, req.user_input, response_text)
1645
+ _dispatch_background(_learn_user_style, req.request_id, req.user_input, response_text, runtime_context)
1646
+
1647
+ return {
1648
+ "status": "ok",
1649
+ "mode": mode,
1650
+ "reply": response_text,
1651
+ "plan": plan_steps,
1652
+ "rationale": rationale,
1653
+ "execution": execution,
1654
+ "knowledge": knowledge,
1655
+ "web_results": web_results,
1656
+ "timestamp": time.time(),
1657
+ }
1658
+ except Exception as exc:
1659
+ logger.exception("Chat failed")
1660
+ return {
1661
+ "status": "error",
1662
+ "mode": "chat",
1663
+ "reply": "حدث خطأ أثناء معالجة الرسالة." if _contains_arabic(req.user_input) else "Chat processing failed.",
1664
+ "detail": str(exc),
1665
+ "timestamp": time.time(),
1666
+ }
1667
+
1668
+
1669
+ @app.post("/init-connection")
1670
+ async def init_connection(payload: ConnectionInit):
1671
+ os.environ["EXECUTOR_URL"] = payload.executor_url
1672
+ FIREBASE.set_document("runtime", "executor", {"executor_url": payload.executor_url})
1673
+ public_url = _report_known_public_url()
1674
+ if not public_url:
1675
+ public_url = start_ngrok(payload.ngrok_token)
1676
+ return {"status": "connected", "brain_public_url": public_url}
1677
+
1678
+
1679
+ @app.post("/system/publish-url")
1680
+ async def system_publish_url(req: PublishUrlRequest | None = None):
1681
+ payload = req or PublishUrlRequest()
1682
+ explicit_public_url = str(payload.public_url or "").strip().rstrip("/")
1683
+ if explicit_public_url:
1684
+ _remember_public_url(explicit_public_url)
1685
+ _report_brain_url(explicit_public_url)
1686
+ FIREBASE.set_document("brains", explicit_public_url, {"url": explicit_public_url, "status": "healthy", "source": "explicit_publish"})
1687
+ return {"status": "published", "brain_public_url": explicit_public_url, "mode": "explicit"}
1688
+
1689
+ public_url = _report_known_public_url()
1690
+ if public_url:
1691
+ return {"status": "published", "brain_public_url": public_url, "mode": "saved"}
1692
+
1693
+ if not payload.start_tunnel:
1694
+ return {"status": "skipped", "brain_public_url": None, "mode": "none"}
1695
+
1696
+ public_url = start_ngrok(payload.ngrok_token)
1697
+ return {"status": "published" if public_url else "error", "brain_public_url": public_url, "mode": "ngrok"}
1698
+
1699
+
1700
+ @app.get("/system/files")
1701
+ async def system_files(path: str = "", include_content: bool = False):
1702
+ try:
1703
+ target = _resolve_sync_path(path)
1704
+ if not target.exists():
1705
+ return {"status": "error", "detail": "Path not found", "path": path}
1706
+ if target.is_file():
1707
+ payload = {"status": "ok", "entry": _describe_sync_entry(target)}
1708
+ if include_content:
1709
+ payload["content"] = target.read_text(encoding="utf-8", errors="ignore")
1710
+ return payload
1711
+ items = sorted((_describe_sync_entry(item) for item in target.iterdir()), key=lambda item: (not item["is_dir"], item["name"].lower()))
1712
+ return {"status": "ok", "root": str(_sync_root_path()), "path": path, "items": items}
1713
+ except Exception as exc:
1714
+ logger.exception("File listing failed")
1715
+ return {"status": "error", "detail": str(exc), "path": path}
1716
+
1717
+
1718
+ @app.post("/system/files/write")
1719
+ async def system_files_write(payload: FileWriteRequest):
1720
+ try:
1721
+ target = _resolve_sync_path(payload.path)
1722
+ target.parent.mkdir(parents=True, exist_ok=True)
1723
+ if target.exists() and target.is_dir():
1724
+ return {"status": "error", "detail": "Target path is a directory"}
1725
+ if target.exists() and not payload.overwrite:
1726
+ return {"status": "error", "detail": "File already exists"}
1727
+ target.write_text(payload.content or "", encoding="utf-8")
1728
+ return {"status": "saved", "entry": _describe_sync_entry(target)}
1729
+ except Exception as exc:
1730
+ logger.exception("File write failed")
1731
+ return {"status": "error", "detail": str(exc), "path": payload.path}
1732
+
1733
+
1734
+ @app.post("/system/files/mkdir")
1735
+ async def system_files_mkdir(payload: FileMkdirRequest):
1736
+ try:
1737
+ target = _resolve_sync_path(payload.path)
1738
+ target.mkdir(parents=True, exist_ok=True)
1739
+ return {"status": "created", "entry": _describe_sync_entry(target)}
1740
+ except Exception as exc:
1741
+ logger.exception("Directory creation failed")
1742
+ return {"status": "error", "detail": str(exc), "path": payload.path}
1743
+
1744
+
1745
+ @app.delete("/system/files")
1746
+ async def system_files_delete(payload: FileDeleteRequest):
1747
+ try:
1748
+ target = _resolve_sync_path(payload.path)
1749
+ if not target.exists():
1750
+ return {"status": "deleted", "path": payload.path, "existed": False}
1751
+ if target.is_dir():
1752
+ if payload.recursive:
1753
+ shutil.rmtree(target)
1754
+ else:
1755
+ target.rmdir()
1756
+ else:
1757
+ target.unlink()
1758
+ return {"status": "deleted", "path": payload.path, "existed": True}
1759
+ except Exception as exc:
1760
+ logger.exception("Delete failed")
1761
+ return {"status": "error", "detail": str(exc), "path": payload.path}
1762
+
1763
+
1764
+ if HAS_MULTIPART:
1765
+ @app.post("/system/sync")
1766
+ async def sync_codebase(file: UploadFile = File(...), restart: bool = False):
1767
+ temp_zip = os.path.join(tempfile.gettempdir(), "kapo_update.zip")
1768
+ try:
1769
+ with open(temp_zip, "wb") as buffer:
1770
+ shutil.copyfileobj(file.file, buffer)
1771
+ with zipfile.ZipFile(temp_zip, "r") as zip_ref:
1772
+ zip_ref.extractall(_sync_target_root())
1773
+ if restart:
1774
+ _restart_process()
1775
+ return {"status": "synced", "target_root": _sync_target_root(), "restart_scheduled": restart}
1776
+ except Exception as exc:
1777
+ logger.exception("Code sync failed")
1778
+ return {"status": "error", "detail": str(exc)}
1779
+
1780
+ @app.post("/system/archive/upload")
1781
+ async def system_archive_upload(file: UploadFile = File(...), target_path: str = "", restart: bool = False):
1782
+ temp_zip = os.path.join(tempfile.gettempdir(), "kapo_archive_upload.zip")
1783
+ try:
1784
+ extract_root = _resolve_sync_path(target_path)
1785
+ extract_root.mkdir(parents=True, exist_ok=True)
1786
+ with open(temp_zip, "wb") as buffer:
1787
+ shutil.copyfileobj(file.file, buffer)
1788
+ with zipfile.ZipFile(temp_zip, "r") as zip_ref:
1789
+ zip_ref.extractall(extract_root)
1790
+ if restart:
1791
+ _restart_process()
1792
+ return {
1793
+ "status": "extracted",
1794
+ "target_root": str(extract_root),
1795
+ "restart_scheduled": restart,
1796
+ }
1797
+ except Exception as exc:
1798
+ logger.exception("Archive upload failed")
1799
+ return {"status": "error", "detail": str(exc)}
1800
+ else:
1801
+ @app.post("/system/sync")
1802
+ async def sync_codebase_unavailable():
1803
+ return {"status": "error", "detail": "python-multipart is required for /system/sync"}
1804
+
1805
+ @app.post("/system/archive/upload")
1806
+ async def system_archive_upload_unavailable():
1807
+ return {"status": "error", "detail": "python-multipart is required for /system/archive/upload"}
1808
+
1809
+
1810
+ @app.post("/system/restart")
1811
+ async def system_restart(req: RestartRequest | None = None):
1812
+ delay_sec = req.delay_sec if req else 1.0
1813
+ if _is_hf_space_runtime():
1814
+ return {
1815
+ "status": "skipped",
1816
+ "reason": "restart_disabled_on_hf_space",
1817
+ "delay_sec": delay_sec,
1818
+ "target_root": _sync_target_root(),
1819
+ }
1820
+ _restart_process(delay_sec=delay_sec)
1821
+ return {
1822
+ "status": "restarting",
1823
+ "delay_sec": delay_sec,
1824
+ "target_root": _sync_target_root(),
1825
+ }
1826
+
1827
+
1828
+ @app.get("/health")
1829
+ async def health(executor_url: str | None = None, check_executor: bool = False):
1830
+ cfg = load_config()
1831
+ base_exec_url = (executor_url or os.getenv("EXECUTOR_URL", "")).strip().rstrip("/")
1832
+ exec_ok = False
1833
+ exec_checked = False
1834
+ exec_error = None
1835
+ health_timeout = int(cfg.get("REQUEST_TIMEOUT_SEC", 20) or 20)
1836
+ health_retries = max(1, int(cfg.get("REQUEST_RETRIES", 2) or 2))
1837
+
1838
+ if base_exec_url and check_executor:
1839
+ exec_checked = True
1840
+ for _ in range(health_retries):
1841
+ try:
1842
+ response = requests.get(
1843
+ f"{base_exec_url}/health",
1844
+ headers=get_executor_headers(cfg),
1845
+ timeout=health_timeout,
1846
+ )
1847
+ exec_ok = response.status_code == 200
1848
+ if exec_ok:
1849
+ exec_error = None
1850
+ break
1851
+ exec_error = response.text
1852
+ except Exception as exc:
1853
+ exec_error = str(exc)
1854
+
1855
+ faiss_path = cfg.get("FAISS_INDEX_PATH")
1856
+ payload = {
1857
+ "status": "ok",
1858
+ "model_loaded": MODEL is not None,
1859
+ "model_error": MODEL_ERROR,
1860
+ "embedding_loaded": EMBED_MODEL is not None,
1861
+ "faiss_ok": bool(faiss_path and os.path.exists(faiss_path)),
1862
+ "executor_checked": exec_checked,
1863
+ "executor_ok": exec_ok,
1864
+ "executor_error": exec_error,
1865
+ "remote_brain_only": str(os.getenv("REMOTE_BRAIN_ONLY", "")).strip().lower() in {"1", "true", "yes", "on"},
1866
+ "runtime_root": os.getenv("KAPO_RUNTIME_ROOT", ""),
1867
+ "sync_root": _sync_target_root(),
1868
+ "timestamp": time.time(),
1869
+ }
1870
+ FIREBASE.set_document(
1871
+ "brains",
1872
+ os.getenv("BRAIN_PUBLIC_URL") or _load_saved_public_url() or os.getenv("KAPO_RUNTIME_ROOT") or "brain_runtime",
1873
+ {
1874
+ "url": os.getenv("BRAIN_PUBLIC_URL") or _load_saved_public_url() or "",
1875
+ "health": payload,
1876
+ "status": "healthy" if payload["model_loaded"] else "degraded",
1877
+ "model_repo": os.getenv("MODEL_REPO", DEFAULT_MODEL_REPO),
1878
+ "model_file": os.getenv("MODEL_FILE", DEFAULT_MODEL_FILE),
1879
+ "model_profile_id": os.getenv("MODEL_PROFILE_ID", DEFAULT_MODEL_PROFILE_ID),
1880
+ "roles": os.getenv("BRAIN_ROLES", "supervisor,chat,coding,planner,arabic,fallback"),
1881
+ "languages": os.getenv("BRAIN_LANGUAGES", "ar,en"),
1882
+ },
1883
+ )
1884
+ return payload
1885
+
1886
+
1887
+ # KAPO HF SPACE TRANSFORMERS PATCH
1888
+ def _kapo_hf_transformers_enabled() -> bool:
1889
+ return str(os.getenv('KAPO_HF_TRANSFORMERS_RUNTIME', '0')).strip().lower() in {'1', 'true', 'yes', 'on'}
1890
+
1891
+ def ensure_model_loaded(repo_id: str, filename: str, hf_token: str | None = None) -> None:
1892
+ global MODEL, MODEL_ERROR, MODEL_META
1893
+ repo_id = (repo_id or '').strip()
1894
+ filename = (filename or '').strip()
1895
+ if not repo_id:
1896
+ MODEL = None
1897
+ MODEL_ERROR = 'model repo missing'
1898
+ return
1899
+ if _kapo_hf_transformers_enabled():
1900
+ try:
1901
+ from transformers import AutoModelForCausalLM, AutoTokenizer
1902
+ tokenizer = AutoTokenizer.from_pretrained(repo_id, token=hf_token, trust_remote_code=True)
1903
+ model = AutoModelForCausalLM.from_pretrained(repo_id, token=hf_token, trust_remote_code=True, device_map='cpu')
1904
+ if hasattr(model, 'eval'):
1905
+ model.eval()
1906
+ MODEL = {'kind': 'transformers', 'model': model, 'tokenizer': tokenizer}
1907
+ MODEL_ERROR = None
1908
+ MODEL_META = {'repo_id': repo_id, 'filename': filename, 'path': None}
1909
+ logger.info('Loaded transformers model %s', repo_id)
1910
+ return
1911
+ except Exception as exc:
1912
+ MODEL = None
1913
+ MODEL_ERROR = f'transformers model load failed: {exc}'
1914
+ logger.exception('Transformers model load failed')
1915
+ return
1916
+ if not filename:
1917
+ MODEL = None
1918
+ MODEL_ERROR = 'model file missing'
1919
+ return
1920
+ try:
1921
+ model_path = _download_model(repo_id, filename, hf_token=hf_token)
1922
+ except Exception as exc:
1923
+ MODEL = None
1924
+ MODEL_ERROR = f'model download failed: {exc}'
1925
+ logger.exception('Model download failed')
1926
+ return
1927
+ try:
1928
+ from llama_cpp import Llama
1929
+ MODEL = Llama(model_path=model_path, n_ctx=4096)
1930
+ MODEL_ERROR = None
1931
+ MODEL_META = {'repo_id': repo_id, 'filename': filename, 'path': model_path}
1932
+ logger.info('Loaded model %s/%s', repo_id, filename)
1933
+ except Exception as exc:
1934
+ MODEL = None
1935
+ MODEL_ERROR = f'model load failed: {exc}'
1936
+ logger.exception('Model load failed')
1937
+
1938
+ def _generate_response(user_input: str, history: list[dict[str, str]], context_block: str) -> str:
1939
+ language = _detect_language(user_input)
1940
+ exact_reply = _extract_exact_reply_instruction_safe(user_input)
1941
+ if exact_reply:
1942
+ return exact_reply
1943
+ fast_reply = _project_specific_fast_reply(user_input)
1944
+ if fast_reply:
1945
+ return fast_reply
1946
+ if MODEL is None:
1947
+ try:
1948
+ _load_default_model()
1949
+ except Exception:
1950
+ logger.exception('Lazy model load failed')
1951
+ if MODEL is None:
1952
+ if language == 'ar':
1953
+ return 'الخدمة تعمل لكن توليد الرد الحر غير متاح الآن لأن النموذج غير محمل.'
1954
+ return 'The Brain is online, but natural chat generation is unavailable because the model is not loaded.'
1955
+ prompt = _build_chat_prompt(user_input, history, context_block)
1956
+ try:
1957
+ max_tokens = 80 if language == 'ar' else 96
1958
+ if isinstance(MODEL, dict) and MODEL.get('kind') == 'transformers':
1959
+ tokenizer = MODEL['tokenizer']
1960
+ model = MODEL['model']
1961
+ inputs = tokenizer(prompt, return_tensors='pt', truncation=True, max_length=2048)
1962
+ if hasattr(model, 'device'):
1963
+ inputs = {k: v.to(model.device) if hasattr(v, 'to') else v for k, v in inputs.items()}
1964
+ output_ids = model.generate(**inputs, max_new_tokens=max_tokens, do_sample=False, pad_token_id=tokenizer.eos_token_id)
1965
+ generated = output_ids[0][inputs['input_ids'].shape[1]:]
1966
+ text = tokenizer.decode(generated, skip_special_tokens=True).strip()
1967
+ else:
1968
+ output = MODEL(prompt, max_tokens=max_tokens, temperature=0.1, top_p=0.85, stop=['\nUser:', '\nUSER:', '\n###', '<|EOT|>'])
1969
+ text = output['choices'][0]['text'].strip()
1970
+ if _response_looks_bad(text, language):
1971
+ return _fallback_response(user_input)
1972
+ return text or ('تم استلام رسالتك.' if language == 'ar' else 'I received your message.')
1973
+ except Exception:
1974
+ logger.exception('Model generation failed')
1975
+ if language == 'ar':
1976
+ return 'فهمت طلبك، لكن حدث خطأ أثناء توليد الرد النصي.'
1977
+ return 'I understood your request, but text generation failed.'
brain_server/api/routes_analyze.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Routes for analyze/store."""
2
+ import time
3
+ import logging
4
+ import os
5
+ from typing import Dict, Any
6
+ from fastapi import APIRouter, HTTPException
7
+ from pydantic import BaseModel
8
+
9
+ from api.deps import get_logger
10
+ import requests
11
+ from memory.episodic_db import EpisodicDB
12
+ from api.deps import is_remote_brain_only, load_config
13
+
14
+ router = APIRouter()
15
+ logger = get_logger("kapo.brain.analyze")
16
+
17
+
18
+ class AnalyzeRequest(BaseModel):
19
+ request_id: str
20
+ payload: Dict[str, Any]
21
+ auth_token: str | None = None
22
+ timestamp: float | None = None
23
+
24
+
25
+ @router.post("/analyze")
26
+ async def analyze(req: AnalyzeRequest):
27
+ """????? ??????? ?? ??????? ??????? (Episodic)."""
28
+ try:
29
+ if not is_remote_brain_only():
30
+ db = EpisodicDB()
31
+ db.insert_experience(
32
+ task=req.payload.get("task", "unknown"),
33
+ plan=req.payload.get("plan", {}),
34
+ tools_used=req.payload.get("tools_used", {}),
35
+ result=req.payload.get("result", {}),
36
+ success=1 if req.payload.get("success") else 0,
37
+ )
38
+ else:
39
+ try:
40
+ hub = load_config().get("LOCAL_HUB_URL") or os.getenv("LOCAL_HUB_URL")
41
+ if hub:
42
+ requests.post(f"{hub}/memory/store", json={"request_id": req.request_id, "payload": req.payload}, timeout=10)
43
+ except Exception:
44
+ logger.warning("Local hub store failed")
45
+ return {"status": "stored", "timestamp": time.time()}
46
+ except Exception as exc:
47
+ logger.exception("Analyze failed")
48
+ raise HTTPException(status_code=500, detail=str(exc))
brain_server/api/routes_execute.py ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Routes for remote execution via the local executive agent."""
2
+ import time
3
+ from typing import Any
4
+
5
+ import requests
6
+ from fastapi import APIRouter, HTTPException
7
+ from pydantic import BaseModel
8
+
9
+ from agents.auto_heal_agent import AutoHealAgent
10
+ from agents.memory_agent import MemoryAgent
11
+ from agents.supervisor_agent import SupervisorAgent
12
+ from agents.tool_selector_agent import ToolSelectorAgent
13
+ from api.deps import get_executor_headers, get_executor_url, get_logger, is_remote_brain_only, load_config
14
+
15
+ router = APIRouter()
16
+ logger = get_logger("kapo.brain.execute")
17
+
18
+
19
+ class ExecuteRequest(BaseModel):
20
+ request_id: str
21
+ plan: dict[str, Any] | list[dict[str, Any]]
22
+ auth_token: str | None = None
23
+ timestamp: float | None = None
24
+ executor_url: str | None = None
25
+
26
+
27
+ def _normalize_executor_url(url: str | None) -> str:
28
+ if not url:
29
+ return ""
30
+ value = url.strip()
31
+ if not value:
32
+ return ""
33
+ if "://" not in value:
34
+ value = f"http://{value}"
35
+ return value.rstrip("/")
36
+
37
+
38
+ def _extract_steps(plan: dict[str, Any] | list[dict[str, Any]]) -> list[dict[str, Any]]:
39
+ if isinstance(plan, list):
40
+ return [_normalize_step(step, index) for index, step in enumerate(plan, start=1)]
41
+ if isinstance(plan, dict):
42
+ steps = plan.get("steps")
43
+ if isinstance(steps, list):
44
+ return [_normalize_step(step, index) for index, step in enumerate(steps, start=1)]
45
+ return []
46
+
47
+
48
+ def _normalize_step(step: dict[str, Any], index: int) -> dict[str, Any]:
49
+ if not isinstance(step, dict):
50
+ return {"id": f"step-{index}", "action": "execute", "input": str(step), "tool_hint": "python"}
51
+ normalized = dict(step)
52
+ if not str(normalized.get("id", "")).strip():
53
+ normalized["id"] = f"step-{index}"
54
+ if not str(normalized.get("input", "")).strip():
55
+ for key in ("description", "title", "summary", "task", "prompt"):
56
+ value = str(normalized.get(key, "")).strip()
57
+ if value:
58
+ normalized["input"] = value
59
+ break
60
+ if not str(normalized.get("action", "")).strip():
61
+ normalized["action"] = "execute"
62
+ if not str(normalized.get("tool_hint", "")).strip():
63
+ normalized["tool_hint"] = "python"
64
+ return normalized
65
+
66
+
67
+ @router.post("/execute")
68
+ async def execute(req: ExecuteRequest):
69
+ cfg = load_config()
70
+ tool_selector = ToolSelectorAgent()
71
+ supervisor = SupervisorAgent()
72
+ auto_heal = AutoHealAgent()
73
+ memory = MemoryAgent()
74
+
75
+ base_exec_url = _normalize_executor_url(req.executor_url) or get_executor_url(cfg)
76
+ exec_url = f"{base_exec_url}/execute"
77
+ exec_headers = get_executor_headers(cfg)
78
+ timeout = int(cfg.get("REQUEST_TIMEOUT_SEC", 20) or 20)
79
+ retries = int(cfg.get("REQUEST_RETRIES", 2) or 2)
80
+
81
+ try:
82
+ results: list[dict[str, Any]] = []
83
+ for step in _extract_steps(req.plan):
84
+ tool = tool_selector.select_tool(step)
85
+ payload = {
86
+ "request_id": req.request_id,
87
+ "step_id": step.get("id"),
88
+ "command": tool.get("command"),
89
+ "files": tool.get("files", {}),
90
+ "env": tool.get("env", {}),
91
+ "timestamp": time.time(),
92
+ }
93
+ attempt = 0
94
+ last_err = None
95
+ while attempt <= retries:
96
+ try:
97
+ response = requests.post(exec_url, json=payload, headers=exec_headers, timeout=timeout)
98
+ if response.status_code == 200:
99
+ item = response.json()
100
+ item["selected_tool"] = tool.get("tool")
101
+ results.append(item)
102
+ last_err = None
103
+ break
104
+ last_err = response.text
105
+ except Exception as exc:
106
+ last_err = str(exc)
107
+ attempt += 1
108
+
109
+ if last_err:
110
+ results.append({"error": last_err, "step": step, "auto_heal": auto_heal.suggest(last_err, step)})
111
+
112
+ report = supervisor.review(results)
113
+ snapshot = {"execution": results, "report": report}
114
+ if not is_remote_brain_only():
115
+ memory.write_short_term(req.request_id, snapshot)
116
+
117
+ return {"status": "ok", "results": results, "report": report, "timestamp": time.time()}
118
+ except Exception as exc:
119
+ logger.exception("Execution failed")
120
+ raise HTTPException(status_code=500, detail=str(exc))
brain_server/api/routes_plan.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Routes for planning."""
2
+ import time
3
+ import uuid
4
+ import logging
5
+ from typing import Dict, Any, List
6
+ from fastapi import APIRouter, HTTPException
7
+ from pydantic import BaseModel
8
+
9
+ from api.deps import get_executor_headers, get_executor_url, get_logger, load_config
10
+ from agents.planner_agent import PlannerAgent
11
+ from agents.reasoning_agent import ReasoningAgent
12
+ from agents.memory_agent import MemoryAgent
13
+ from api.deps import is_remote_brain_only
14
+ import requests
15
+
16
+ router = APIRouter()
17
+ logger = get_logger("kapo.brain.plan")
18
+
19
+
20
+ class PlanRequest(BaseModel):
21
+ request_id: str
22
+ user_input: str
23
+ context: Dict[str, Any] = {}
24
+ auth_token: str | None = None
25
+ timestamp: float | None = None
26
+
27
+
28
+ class PlanResponse(BaseModel):
29
+ plan_id: str
30
+ plan: List[Dict[str, Any]]
31
+ metadata: Dict[str, Any]
32
+
33
+
34
+ @router.post("/plan", response_model=PlanResponse)
35
+ async def plan(req: PlanRequest):
36
+ """????? ??? ????? ???????."""
37
+ try:
38
+ logger.info("Plan requested", extra={"request_id": req.request_id, "component": "plan"})
39
+ planner = PlannerAgent()
40
+ reasoning = ReasoningAgent()
41
+ memory = MemoryAgent()
42
+ plan_steps = planner.run(req.user_input, req.context)
43
+ rationale = reasoning.run(req.user_input, plan_steps)
44
+ plan_id = str(uuid.uuid4())
45
+ if not is_remote_brain_only():
46
+ memory.write_short_term(req.request_id, {"plan": plan_steps, "rationale": rationale})
47
+ else:
48
+ try:
49
+ cfg = load_config()
50
+ hub = get_executor_url(cfg)
51
+ if hub:
52
+ requests.post(
53
+ f"{hub}/memory/store",
54
+ json={"request_id": req.request_id, "payload": {"plan": plan_steps, "rationale": rationale}},
55
+ headers=get_executor_headers(cfg),
56
+ timeout=(3, 4),
57
+ )
58
+ except Exception:
59
+ logger.warning("Local hub store failed")
60
+ return PlanResponse(
61
+ plan_id=plan_id,
62
+ plan=plan_steps,
63
+ metadata={"rationale": rationale, "timestamp": time.time()},
64
+ )
65
+ except Exception as exc:
66
+ logger.exception("Plan failed")
67
+ raise HTTPException(status_code=500, detail=str(exc))
brain_server/config/config.yaml ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ??????? Brain Server
2
+ MODEL_PATH: "${MODEL_PATH}"
3
+ BRAIN_HOST: "${BRAIN_HOST}"
4
+ BRAIN_PORT: "${BRAIN_PORT}"
5
+ EXECUTOR_HOST: "${EXECUTOR_HOST}"
6
+ EXECUTOR_PORT: "${EXECUTOR_PORT}"
7
+ DB_PATH: "${DB_PATH}"
8
+ TOOLS_DB_PATH: "${TOOLS_DB_PATH}"
9
+ FAISS_INDEX_PATH: "${FAISS_INDEX_PATH}"
10
+ EMBED_MODEL: "${EMBED_MODEL}"
11
+ LOG_LEVEL: "${LOG_LEVEL}"
12
+ WHITELIST_COMMANDS: "${WHITELIST_COMMANDS}"
13
+ BLACKLIST_COMMANDS: "${BLACKLIST_COMMANDS}"
14
+ REQUEST_TIMEOUT_SEC: 20
15
+ REQUEST_RETRIES: 2
16
+ REMOTE_BRAIN_ONLY: "${REMOTE_BRAIN_ONLY}"
17
+ BRAIN_REMOTE_KNOWLEDGE_ENABLED: "${BRAIN_REMOTE_KNOWLEDGE_ENABLED}"
18
+ BRAIN_REMOTE_WEB_SEARCH_ENABLED: "${BRAIN_REMOTE_WEB_SEARCH_ENABLED}"
19
+ BRAIN_REMOTE_TRACE_STORE_ENABLED: "${BRAIN_REMOTE_TRACE_STORE_ENABLED}"
20
+ BRAIN_REMOTE_AUTO_INGEST_ENABLED: "${BRAIN_REMOTE_AUTO_INGEST_ENABLED}"
21
+ BRAIN_LOCAL_RAG_FALLBACK_ENABLED: "${BRAIN_LOCAL_RAG_FALLBACK_ENABLED}"
22
+ EXECUTOR_CONNECT_TIMEOUT_SEC: "${EXECUTOR_CONNECT_TIMEOUT_SEC}"
23
+ BRAIN_REMOTE_KNOWLEDGE_TIMEOUT_SEC: "${BRAIN_REMOTE_KNOWLEDGE_TIMEOUT_SEC}"
24
+ BRAIN_REMOTE_WEB_SEARCH_TIMEOUT_SEC: "${BRAIN_REMOTE_WEB_SEARCH_TIMEOUT_SEC}"
25
+ BRAIN_REMOTE_TRACE_STORE_TIMEOUT_SEC: "${BRAIN_REMOTE_TRACE_STORE_TIMEOUT_SEC}"
26
+ BRAIN_REMOTE_AUTO_INGEST_TIMEOUT_SEC: "${BRAIN_REMOTE_AUTO_INGEST_TIMEOUT_SEC}"
brain_server/config/logging.yaml ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: 1
2
+ formatters:
3
+ json:
4
+ class: pythonjsonlogger.jsonlogger.JsonFormatter
5
+ format: "%(asctime)s %(levelname)s %(name)s %(message)s %(request_id)s %(component)s"
6
+ console:
7
+ format: "%(asctime)s | %(levelname)s | %(name)s | %(message)s"
8
+ handlers:
9
+ console:
10
+ class: logging.StreamHandler
11
+ formatter: console
12
+ json:
13
+ class: logging.StreamHandler
14
+ formatter: json
15
+ root:
16
+ level: INFO
17
+ handlers: [console]
18
+ loggers:
19
+ kapo:
20
+ level: INFO
21
+ handlers: [json]
22
+ propagate: false
brain_server/kaggle_bootstrap.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Bootstrap a Kaggle dataset copy of brain_server into /kaggle/working and print run guidance."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ import shutil
6
+ from pathlib import Path
7
+
8
+
9
+ def main() -> None:
10
+ source_root = Path(__file__).resolve().parent
11
+ runtime_root = Path(os.getenv("KAPO_RUNTIME_ROOT") or "/kaggle/working/brain_server").resolve()
12
+
13
+ if source_root != runtime_root:
14
+ if runtime_root.exists():
15
+ shutil.rmtree(runtime_root, ignore_errors=True)
16
+ shutil.copytree(source_root, runtime_root)
17
+
18
+ print(f"Bootstrapped brain_server to: {runtime_root}")
19
+ print("Next steps:")
20
+ print(f" %cd {runtime_root}")
21
+ print(" !pip install --default-timeout=1000 --no-cache-dir -r requirements.txt")
22
+ print(" !python -m uvicorn api.main:app --host 0.0.0.0 --port 7860")
23
+
24
+
25
+ if __name__ == "__main__":
26
+ main()
brain_server/langgraph/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """LangGraph wrapper package."""
brain_server/langgraph/agent_prompts.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Prompts ??? ???? (Arabic + short English example)."""
2
+ import logging
3
+
4
+ logger = logging.getLogger("kapo.prompts")
5
+
6
+ PLANNER_PROMPT = """
7
+ [Planner Agent]
8
+ ??? ????? ?? ????? ???? ???????? ??? ??? ??????? JSON.
9
+
10
+ ??????? (JSON Schema):
11
+ {
12
+ "request_id": "str",
13
+ "user_input": "str",
14
+ "context": {"...": "..."}
15
+ }
16
+
17
+ ??????? ??????? (JSON Schema):
18
+ {
19
+ "steps": [
20
+ {"id": "str", "action": "str", "tool_hint": "str", "files": {}, "env": {}}
21
+ ],
22
+ "assumptions": ["..."]
23
+ }
24
+
25
+ ???? ????:
26
+ Input: {"request_id":"r1","user_input":"??? ????","context":{}}
27
+ Output: {"steps":[{"id":"s1","action":"analyze","tool_hint":"python"}],"assumptions":[]}
28
+
29
+ English short example:
30
+ Input: {"request_id":"r1","user_input":"Summarize logs"}
31
+ Output: {"steps":[{"id":"s1","action":"summarize","tool_hint":"python"}]}
32
+ """
33
+
34
+ REASONING_PROMPT = """
35
+ [Reasoning Agent]
36
+ ??? ????? ???? ??????? ???????.
37
+
38
+ Input Schema:
39
+ {"user_input":"str","plan":{"steps":[...]}}
40
+
41
+ Output Schema:
42
+ {"rationale":"str","risks":["..."],"notes":"str"}
43
+ """
44
+
45
+ TOOL_SELECTOR_PROMPT = """
46
+ [Tool Selector Agent]
47
+ ???? ?????? ?????? ??? ????.
48
+
49
+ Input Schema:
50
+ {"step":{"id":"str","action":"str"},"tools":[{"tool_name":"str"}]}
51
+
52
+ Output Schema:
53
+ {"tool_name":"str","command":"str","reason":"str"}
54
+ """
55
+
56
+ SUPERVISOR_PROMPT = """
57
+ [Supervisor Agent]
58
+ ??? ????? ??????? ???? ?????? ?? ????? ?? ??????.
59
+
60
+ Input Schema:
61
+ {"results":[{"exit_code":0,"stdout":""}]}
62
+
63
+ Output Schema:
64
+ {"success":true,"report":"str","next_actions":["..."]}
65
+ """
66
+
67
+ AUTO_HEAL_PROMPT = """
68
+ [Auto-Heal Agent]
69
+ ??? ??????? ?????? ??????? ????? ???????.
70
+
71
+ Input Schema:
72
+ {"error_text":"str","context":{"step":{}}}
73
+
74
+ Output Schema:
75
+ {"suggested_fix":"str","reexecute":true}
76
+ """
77
+
78
+ MEMORY_PROMPT = """
79
+ [Memory Agent]
80
+ ??? ?? ??? ?????? ?? ??????? ?????? ????????.
81
+
82
+ Input Schema:
83
+ {"event":{...},"policy":"str"}
84
+
85
+ Output Schema:
86
+ {"store_short_term":true,"store_episodic":true,"keys":["..."]}
87
+ """
brain_server/langgraph/graph_definition.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """????? ???? LangGraph ?? ???? ????."""
2
+ import logging
3
+ from typing import Dict, Any
4
+ from agents.planner_agent import PlannerAgent
5
+ from agents.reasoning_agent import ReasoningAgent
6
+ from agents.tool_selector_agent import ToolSelectorAgent
7
+ from agents.supervisor_agent import SupervisorAgent
8
+ from agents.auto_heal_agent import AutoHealAgent
9
+ from agents.memory_agent import MemoryAgent
10
+ from api.deps import get_logger
11
+
12
+ logger = get_logger("kapo.langgraph")
13
+
14
+
15
+ class SimpleGraph:
16
+ """???? ???? ?? LangGraph ??? ???? ???????."""
17
+
18
+ def run(self, user_input: str, context: Dict[str, Any]):
19
+ planner = PlannerAgent()
20
+ reasoning = ReasoningAgent()
21
+ tool_selector = ToolSelectorAgent()
22
+ supervisor = SupervisorAgent()
23
+ auto_heal = AutoHealAgent()
24
+ memory = MemoryAgent()
25
+
26
+ plan = planner.run(user_input, context)
27
+ rationale = reasoning.run(user_input, plan)
28
+ memory.write_short_term("last_plan", {"plan": plan, "rationale": rationale})
29
+ return {
30
+ "plan": plan,
31
+ "rationale": rationale,
32
+ "tool_selector": tool_selector,
33
+ "supervisor": supervisor,
34
+ "auto_heal": auto_heal,
35
+ }
36
+
37
+
38
+ def get_graph():
39
+ """????? ??????? LangGraph ?? ????? ???? ?????? SimpleGraph."""
40
+ try:
41
+ import langgraph # noqa: F401
42
+ logger.info("Using langgraph")
43
+ return SimpleGraph()
44
+ except Exception:
45
+ logger.warning("LangGraph not available; using SimpleGraph")
46
+ return SimpleGraph()
brain_server/memory/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Memory package."""
brain_server/memory/episodic_db.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Episodic SQLite storage."""
2
+ import json
3
+ import sqlite3
4
+ import time
5
+ import logging
6
+ from typing import Dict, Any, List
7
+ from api.deps import load_config, get_logger
8
+
9
+ logger = get_logger("kapo.memory.episodic")
10
+
11
+
12
+ class EpisodicDB:
13
+ def __init__(self):
14
+ cfg = load_config()
15
+ self.db_path = cfg.get("DB_PATH") or "./episodic.db"
16
+ self._init_db()
17
+
18
+ def _init_db(self):
19
+ conn = sqlite3.connect(self.db_path)
20
+ cur = conn.cursor()
21
+ cur.execute(
22
+ """
23
+ CREATE TABLE IF NOT EXISTS experiences (
24
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
25
+ task TEXT,
26
+ plan TEXT,
27
+ tools_used TEXT,
28
+ result TEXT,
29
+ success INTEGER,
30
+ timestamp TEXT
31
+ )
32
+ """
33
+ )
34
+ conn.commit()
35
+ conn.close()
36
+
37
+ def insert_experience(self, task: str, plan: Dict[str, Any], tools_used: Dict[str, Any], result: Dict[str, Any], success: int):
38
+ conn = sqlite3.connect(self.db_path)
39
+ cur = conn.cursor()
40
+ cur.execute(
41
+ """INSERT INTO experiences(task, plan, tools_used, result, success, timestamp)
42
+ VALUES(?,?,?,?,?,?)""",
43
+ (task, json.dumps(plan), json.dumps(tools_used), json.dumps(result), success, time.strftime("%Y-%m-%dT%H:%M:%S")),
44
+ )
45
+ conn.commit()
46
+ conn.close()
47
+
48
+ def list_recent(self, limit: int = 20) -> List[Dict[str, Any]]:
49
+ conn = sqlite3.connect(self.db_path)
50
+ cur = conn.cursor()
51
+ cur.execute("SELECT task, plan, tools_used, result, success, timestamp FROM experiences ORDER BY id DESC LIMIT ?", (limit,))
52
+ rows = cur.fetchall()
53
+ conn.close()
54
+ out = []
55
+ for r in rows:
56
+ out.append({
57
+ "task": r[0],
58
+ "plan": json.loads(r[1]),
59
+ "tools_used": json.loads(r[2]),
60
+ "result": json.loads(r[3]),
61
+ "success": r[4],
62
+ "timestamp": r[5],
63
+ })
64
+ return out
65
+
66
+ def search_similar(self, text: str, top_k: int = 3):
67
+ """??? ?????? ??? embeddings ?????? (?????? ?????)."""
68
+ try:
69
+ from sentence_transformers import SentenceTransformer
70
+ cfg = load_config()
71
+ model = cfg.get("EMBED_MODEL") or "sentence-transformers/all-MiniLM-L6-v2"
72
+ embedder = SentenceTransformer(model)
73
+ records = self.list_recent(limit=200)
74
+ if not records:
75
+ return []
76
+ texts = [r.get("task", "") for r in records]
77
+ vectors = embedder.encode(texts, show_progress_bar=False)
78
+ qv = embedder.encode([text])[0]
79
+ # cosine similarity
80
+ def cos(a, b):
81
+ import math
82
+ dot = sum(x*y for x, y in zip(a, b))
83
+ na = math.sqrt(sum(x*x for x in a))
84
+ nb = math.sqrt(sum(x*x for x in b))
85
+ return dot / (na*nb + 1e-9)
86
+ scored = [(cos(qv, v), r) for v, r in zip(vectors, records)]
87
+ scored.sort(key=lambda x: x[0], reverse=True)
88
+ return [r for _, r in scored[:top_k]]
89
+ except Exception:
90
+ logger.exception("Similarity search failed")
91
+ return []
brain_server/memory/knowledge_vector.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Knowledge vector store using sentence-transformers + FAISS."""
2
+ import os
3
+ import sqlite3
4
+ import logging
5
+ from typing import List
6
+ from api.deps import load_config, get_logger
7
+
8
+ logger = get_logger("kapo.memory.knowledge")
9
+
10
+
11
+ class KnowledgeVectorStore:
12
+ def __init__(self):
13
+ cfg = load_config()
14
+ self.index_path = cfg.get("FAISS_INDEX_PATH") or "./faiss.index"
15
+ self.meta_db = self.index_path + ".meta.db"
16
+ self.embed_model = cfg.get("EMBED_MODEL") or "sentence-transformers/all-MiniLM-L6-v2"
17
+ self._init_meta()
18
+
19
+ def _init_meta(self):
20
+ conn = sqlite3.connect(self.meta_db)
21
+ cur = conn.cursor()
22
+ cur.execute(
23
+ """
24
+ CREATE TABLE IF NOT EXISTS vectors (
25
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
26
+ source TEXT,
27
+ content TEXT
28
+ )
29
+ """
30
+ )
31
+ conn.commit()
32
+ conn.close()
33
+
34
+ def _load_embedder(self):
35
+ from sentence_transformers import SentenceTransformer
36
+ return SentenceTransformer(self.embed_model)
37
+
38
+ def _load_index(self, dim: int):
39
+ import faiss
40
+ if os.path.exists(self.index_path):
41
+ return faiss.read_index(self.index_path)
42
+ return faiss.IndexFlatL2(dim)
43
+
44
+ def add_texts(self, texts: List[str], source: str = "unknown"):
45
+ try:
46
+ embedder = self._load_embedder()
47
+ embeddings = embedder.encode(texts, show_progress_bar=False)
48
+ dim = len(embeddings[0])
49
+ index = self._load_index(dim)
50
+ index.add(embeddings)
51
+ import faiss
52
+ faiss.write_index(index, self.index_path)
53
+
54
+ conn = sqlite3.connect(self.meta_db)
55
+ cur = conn.cursor()
56
+ for t in texts:
57
+ cur.execute("INSERT INTO vectors(source, content) VALUES(?,?)", (source, t))
58
+ conn.commit()
59
+ conn.close()
60
+ except Exception:
61
+ logger.exception("Failed to add texts")
62
+
63
+ def query(self, q: str, top_k: int = 3):
64
+ try:
65
+ embedder = self._load_embedder()
66
+ qv = embedder.encode([q])
67
+ import faiss
68
+ if not os.path.exists(self.index_path):
69
+ return []
70
+ index = faiss.read_index(self.index_path)
71
+ scores, ids = index.search(qv, top_k)
72
+ conn = sqlite3.connect(self.meta_db)
73
+ cur = conn.cursor()
74
+ results = []
75
+ for idx in ids[0]:
76
+ cur.execute("SELECT source, content FROM vectors WHERE id=?", (int(idx) + 1,))
77
+ row = cur.fetchone()
78
+ if row:
79
+ results.append({"source": row[0], "content": row[1]})
80
+ conn.close()
81
+ return results
82
+ except Exception:
83
+ logger.exception("Query failed")
84
+ return []
brain_server/memory/short_term.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Short-term in-memory store with TTL."""
2
+ import time
3
+ import logging
4
+ from typing import Any, Dict
5
+
6
+ logger = logging.getLogger("kapo.memory.short_term")
7
+
8
+
9
+ class ShortTermMemory:
10
+ def __init__(self, ttl_sec: int = 1800, max_items: int = 500):
11
+ self.ttl_sec = ttl_sec
12
+ self.max_items = max_items
13
+ self._store: Dict[str, Any] = {}
14
+ self._ts: Dict[str, float] = {}
15
+
16
+ def _cleanup(self):
17
+ try:
18
+ now = time.time()
19
+ expired = [k for k, t in self._ts.items() if now - t > self.ttl_sec]
20
+ for k in expired:
21
+ self._store.pop(k, None)
22
+ self._ts.pop(k, None)
23
+ if len(self._store) > self.max_items:
24
+ for k in list(self._store.keys())[: len(self._store) - self.max_items]:
25
+ self._store.pop(k, None)
26
+ self._ts.pop(k, None)
27
+ except Exception:
28
+ logger.exception("Cleanup failed")
29
+
30
+ def set(self, key: str, value: Any):
31
+ self._store[key] = value
32
+ self._ts[key] = time.time()
33
+ self._cleanup()
34
+
35
+ def get(self, key: str):
36
+ self._cleanup()
37
+ return self._store.get(key)
brain_server/rag/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """RAG package."""
brain_server/rag/loader.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """RAG Loader: ????? ????? ?? ???????."""
2
+ import os
3
+ import logging
4
+ from typing import List
5
+ from api.deps import get_logger
6
+
7
+ logger = get_logger("kapo.rag.loader")
8
+
9
+
10
+ def load_texts(paths: List[str]) -> List[str]:
11
+ texts = []
12
+ for p in paths:
13
+ try:
14
+ if os.path.exists(p):
15
+ with open(p, "r", encoding="utf-8") as f:
16
+ texts.append(f.read())
17
+ except Exception:
18
+ logger.exception("Failed to load %s", p)
19
+ return texts
brain_server/rag/retriever.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """RAG Retriever: ??????? FAISS."""
2
+ import logging
3
+ from memory.knowledge_vector import KnowledgeVectorStore
4
+
5
+ logger = logging.getLogger("kapo.rag.retriever")
6
+
7
+
8
+ def retrieve(query: str, top_k: int = 3):
9
+ try:
10
+ store = KnowledgeVectorStore()
11
+ return store.query(query, top_k=top_k)
12
+ except Exception:
13
+ logger.exception("Retrieve failed")
14
+ return []
brain_server/requirements.txt ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ numpy<2.0.0
2
+ fastapi>=0.115.2,<1.0
3
+ uvicorn==0.30.1
4
+ pydantic==2.7.3
5
+ python-dotenv==1.0.1
6
+ requests==2.32.3
7
+ python-multipart>=0.0.18
8
+ sqlalchemy==2.0.30
9
+ sentence-transformers==3.0.1
10
+ faiss-cpu==1.8.0
11
+ llama-cpp-python==0.2.79
12
+ python-json-logger==2.0.7
13
+ langgraph==0.0.50
14
+ huggingface_hub>=0.33.5,<2.0
15
+ pyngrok==7.1.6
16
+ firebase-admin==6.5.0
17
+ starlette>=0.40.0,<1.0
18
+
19
+ sentence-transformers
20
+ transformers
21
+
22
+ peft
23
+ bitsandbytes
24
+ trl
25
+ accelerate
26
+ datasets
requirements.txt ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ numpy<2.0.0
2
+ fastapi>=0.115.2,<1.0
3
+ uvicorn==0.30.1
4
+ pydantic==2.7.3
5
+ python-dotenv==1.0.1
6
+ requests==2.32.3
7
+ python-multipart>=0.0.18
8
+ sqlalchemy==2.0.30
9
+ sentence-transformers==3.0.1
10
+ faiss-cpu==1.8.0
11
+ python-json-logger==2.0.7
12
+ langgraph==0.0.50
13
+ huggingface_hub>=0.33.5,<2.0
14
+ firebase-admin==6.5.0
15
+ starlette>=0.40.0,<1.0
16
+ transformers>=4.46.0,<5.0
17
+ accelerate>=0.34.0,<1.0
18
+ datasets>=2.21.0,<4.0