MrA7A1 commited on
Commit
06ce7ac
·
verified ·
1 Parent(s): f2e871d

Initial modernized KAPO runtime upload

Browse files
.env ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ BRAIN_AUTO_NGROK=0
2
+ BRAIN_AUTO_PUBLISH_URL_ON_STARTUP=0
3
+ BRAIN_LANGUAGES=ar,en
4
+ BRAIN_PLATFORM_NAME=ai_coder_main
5
+ BRAIN_PROVIDER=huggingface
6
+ BRAIN_REUSE_PUBLIC_URL_ON_RESTART=0
7
+ BRAIN_ROLES=coding,planner,fallback
8
+ BRAIN_TEMPLATE=hf-space-cpu
9
+ BRAIN_TUNNEL_PROVIDER=none
10
+ FIREBASE_ENABLED=0
11
+ FIREBASE_NAMESPACE=kapo
12
+ FIREBASE_PROJECT_ID=citadel4travels
13
+ GOOGLE_DRIVE_BOOTSTRAP_URL=https://drive.google.com/uc?export=download&id=19jyBWsQ9ciJVPi2PUigu5ti3gJ24A6TG
14
+ HF_ACCELERATOR=cpu
15
+ HF_SPACE_DOCKER=1
16
+ KAGGLE_AUTO_BOOTSTRAP=0
17
+ KAPO_BOOTSTRAP_URL=https://drive.google.com/uc?export=download&id=19jyBWsQ9ciJVPi2PUigu5ti3gJ24A6TG
18
+ KAPO_COMPUTE_PROFILE=cpu
19
+ KAPO_HF_INFERENCE_API=1
20
+ KAPO_HF_TRANSFORMERS_RUNTIME=0
21
+ KAPO_LAZY_EMBED_STARTUP=1
22
+ KAPO_LAZY_MODEL_STARTUP=1
23
+ KAPO_PATCH_BUNDLE_URL=https://drive.google.com/uc?export=download&id=16rIe05GZihhAz7ba8E-WibKaJKbh9eu1
24
+ KAPO_PATCH_MANIFEST_URL=https://drive.google.com/uc?export=download&id=1jLuPMCA3hp9qstZZtpBNzTK0XOmLrV8b
25
+ KAPO_REMOTE_ENV_PASSWORD_B64=cENvSnljQ0Z6RUN2azRpcUFLVUdQLW9BbVBoTEtNOFE
26
+ KAPO_REMOTE_ENV_URL_B64=aHR0cHM6Ly9kcml2ZS5nb29nbGUuY29tL3VjP2V4cG9ydD1kb3dubG9hZCZpZD0xdjlZRDlNYlBKNUdYNk9WanVHM3hOR05xLWJfQlJfTGY
27
+ KAPO_SHARED_STATE_BACKEND=google_drive
28
+ MODEL_PROFILE_ID=hf-coder-qwen25-coder-7b-instruct
29
+ MODEL_REPO=Qwen/Qwen2.5-Coder-1.5B-Instruct
30
+ REMOTE_BRAIN_ONLY=1
31
+ SPACE_PUBLIC_URL=https://MrA7A1-AiCoder.hf.space
Dockerfile ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 shared /app/shared
10
+ COPY .env /app/.env
11
+ COPY kapo.env /app/kapo.env
12
+ COPY bootstrap_space_runtime.py /app/bootstrap_space_runtime.py
13
+ CMD ["python", "/app/bootstrap_space_runtime.py"]
README.md CHANGED
@@ -1,10 +1,21 @@
1
- ---
2
- title: AiCoderClean
3
- emoji: 👀
4
- colorFrom: pink
5
- colorTo: green
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: ai_coder_main
3
+ emoji: 🤖
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: docker
7
+ pinned: false
8
+ ---
9
+
10
+ # ai_coder_main
11
+
12
+ Generated Hugging Face deployment package from KAPO Control Center.
13
+
14
+ Model profile: hf-coder-qwen25-coder-7b-instruct
15
+ Model repo: Qwen/Qwen2.5-Coder-1.5B-Instruct
16
+ Model file: not set
17
+ Roles: coding, planner, 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,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ "BRAIN_AUTO_NGROK": "0",
10
+ "BRAIN_AUTO_PUBLISH_URL_ON_STARTUP": "0",
11
+ "BRAIN_LANGUAGES": "ar,en",
12
+ "BRAIN_PLATFORM_NAME": "ai_coder_main",
13
+ "BRAIN_PROVIDER": "huggingface",
14
+ "BRAIN_REUSE_PUBLIC_URL_ON_RESTART": "0",
15
+ "BRAIN_ROLES": "coding,planner,fallback",
16
+ "BRAIN_TEMPLATE": "hf-space-cpu",
17
+ "BRAIN_TUNNEL_PROVIDER": "none",
18
+ "FIREBASE_ENABLED": "0",
19
+ "FIREBASE_NAMESPACE": "kapo",
20
+ "FIREBASE_PROJECT_ID": "citadel4travels",
21
+ "GOOGLE_DRIVE_BOOTSTRAP_URL": "https://drive.google.com/uc?export=download&id=19jyBWsQ9ciJVPi2PUigu5ti3gJ24A6TG",
22
+ "HF_ACCELERATOR": "cpu",
23
+ "HF_SPACE_DOCKER": "1",
24
+ "KAGGLE_AUTO_BOOTSTRAP": "0",
25
+ "KAPO_BOOTSTRAP_URL": "https://drive.google.com/uc?export=download&id=19jyBWsQ9ciJVPi2PUigu5ti3gJ24A6TG",
26
+ "KAPO_COMPUTE_PROFILE": "cpu",
27
+ "KAPO_HF_INFERENCE_API": "1",
28
+ "KAPO_HF_TRANSFORMERS_RUNTIME": "0",
29
+ "KAPO_LAZY_EMBED_STARTUP": "1",
30
+ "KAPO_LAZY_MODEL_STARTUP": "1",
31
+ "KAPO_PATCH_BUNDLE_URL": "https://drive.google.com/uc?export=download&id=16rIe05GZihhAz7ba8E-WibKaJKbh9eu1",
32
+ "KAPO_PATCH_MANIFEST_URL": "https://drive.google.com/uc?export=download&id=1jLuPMCA3hp9qstZZtpBNzTK0XOmLrV8b",
33
+ "KAPO_REMOTE_ENV_PASSWORD_B64": "cENvSnljQ0Z6RUN2azRpcUFLVUdQLW9BbVBoTEtNOFE",
34
+ "KAPO_REMOTE_ENV_URL_B64": "aHR0cHM6Ly9kcml2ZS5nb29nbGUuY29tL3VjP2V4cG9ydD1kb3dubG9hZCZpZD0xdjlZRDlNYlBKNUdYNk9WanVHM3hOR05xLWJfQlJfTGY",
35
+ "KAPO_SHARED_STATE_BACKEND": "google_drive",
36
+ "MODEL_PROFILE_ID": "hf-coder-qwen25-coder-7b-instruct",
37
+ "MODEL_REPO": "Qwen/Qwen2.5-Coder-1.5B-Instruct",
38
+ "REMOTE_BRAIN_ONLY": "1",
39
+ "SPACE_PUBLIC_URL": "https://MrA7A1-AiCoder.hf.space"
40
+ }
41
+
42
+
43
+ def _copy_tree(source: Path, target: Path) -> None:
44
+ if target.exists():
45
+ shutil.rmtree(target, ignore_errors=True)
46
+ shutil.copytree(
47
+ source,
48
+ target,
49
+ ignore=shutil.ignore_patterns('__pycache__', '*.pyc', '.git', '.venv'),
50
+ )
51
+
52
+
53
+ def _merge_overlay(overlay_root: Path, runtime_root: Path) -> None:
54
+ if not overlay_root.exists():
55
+ return
56
+ for path in sorted(overlay_root.rglob('*')):
57
+ rel = path.relative_to(overlay_root)
58
+ dst = runtime_root / rel
59
+ if path.is_dir():
60
+ dst.mkdir(parents=True, exist_ok=True)
61
+ continue
62
+ dst.parent.mkdir(parents=True, exist_ok=True)
63
+ shutil.copy2(path, dst)
64
+
65
+
66
+ def main() -> None:
67
+ source_root = Path(os.getenv('KAPO_SPACE_SOURCE_ROOT', '/app')).resolve()
68
+ default_root = Path('/data/kapo_runtime/current') if Path('/data').exists() else Path('/tmp/kapo_runtime/current')
69
+ runtime_root = Path(os.getenv('KAPO_RUNTIME_ROOT', str(default_root))).resolve()
70
+ overlay_root = Path(os.getenv('KAPO_OVERLAY_ROOT', str(runtime_root.parent / 'overlay'))).resolve()
71
+ runtime_pkg = runtime_root / 'brain_server'
72
+ source_pkg = source_root / 'brain_server'
73
+ for key, value in DEFAULT_ENV.items():
74
+ os.environ.setdefault(str(key), str(value))
75
+ space_host = str(os.getenv('SPACE_HOST', '')).strip().rstrip('/')
76
+ space_id = str(os.getenv('SPACE_ID', '')).strip()
77
+ public_url = ''
78
+ if space_host:
79
+ public_url = space_host if space_host.startswith('http://') or space_host.startswith('https://') else f'https://{space_host}'
80
+ elif space_id and '/' in space_id:
81
+ public_url = 'https://' + space_id.replace('/', '-').lower() + '.hf.space'
82
+ if public_url:
83
+ os.environ['BRAIN_PUBLIC_URL'] = public_url.rstrip('/')
84
+ runtime_root.mkdir(parents=True, exist_ok=True)
85
+ overlay_root.mkdir(parents=True, exist_ok=True)
86
+ if not runtime_pkg.exists():
87
+ _copy_tree(source_pkg, runtime_pkg)
88
+ _merge_overlay(overlay_root, runtime_root)
89
+ os.environ['KAPO_RUNTIME_ROOT'] = str(runtime_root)
90
+ os.environ['KAPO_SYNC_ROOT'] = str(runtime_root)
91
+ os.environ['KAPO_OVERLAY_ROOT'] = str(overlay_root)
92
+ port = str(os.getenv('PORT', '7860') or '7860')
93
+ os.execvp(
94
+ sys.executable,
95
+ [
96
+ sys.executable,
97
+ '-m',
98
+ 'uvicorn',
99
+ 'api.main:app',
100
+ '--host',
101
+ '0.0.0.0',
102
+ '--port',
103
+ port,
104
+ '--app-dir',
105
+ str(runtime_pkg),
106
+ ],
107
+ )
108
+
109
+
110
+ if __name__ == '__main__':
111
+ 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,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ from shared.remote_env import load_remote_env_if_configured
11
+
12
+ CONFIG_CACHE: dict[str, Any] | None = None
13
+ _LOGGING_READY = False
14
+ PLACEHOLDER_RE = re.compile(r"^\$\{[A-Z0-9_]+\}$")
15
+
16
+
17
+ def _load_env_stack() -> None:
18
+ candidates = [
19
+ ".env",
20
+ "kapo.env",
21
+ ".env.runtime",
22
+ ]
23
+ for candidate in candidates:
24
+ try:
25
+ load_dotenv(candidate, override=True)
26
+ except Exception:
27
+ continue
28
+
29
+
30
+ def _normalize_config_paths(cfg: dict[str, Any]) -> dict[str, Any]:
31
+ if os.name != "nt":
32
+ return cfg
33
+
34
+ root = os.getcwd()
35
+ normalized = dict(cfg)
36
+ path_keys = {
37
+ "DB_PATH",
38
+ "TOOLS_DB_PATH",
39
+ "FAISS_INDEX_PATH",
40
+ "BRAIN_LOG_PATH",
41
+ "EXEC_LOG_PATH",
42
+ "LOCAL_DATA_DIR",
43
+ }
44
+ for key in path_keys:
45
+ value = normalized.get(key)
46
+ if not isinstance(value, str) or not value:
47
+ continue
48
+ if value.startswith("/data"):
49
+ normalized[key] = os.path.join(root, "data", value[len("/data"):].lstrip("/\\"))
50
+ elif value.startswith("/models"):
51
+ normalized[key] = os.path.join(root, "models", value[len("/models"):].lstrip("/\\"))
52
+ return normalized
53
+
54
+
55
+ def _strip_unresolved_placeholders(value):
56
+ if isinstance(value, dict):
57
+ return {key: _strip_unresolved_placeholders(item) for key, item in value.items()}
58
+ if isinstance(value, list):
59
+ return [_strip_unresolved_placeholders(item) for item in value]
60
+ if isinstance(value, str) and PLACEHOLDER_RE.match(value.strip()):
61
+ return ""
62
+ return value
63
+
64
+
65
+ def load_config() -> dict:
66
+ global CONFIG_CACHE
67
+ if CONFIG_CACHE is not None:
68
+ return CONFIG_CACHE
69
+
70
+ _load_env_stack()
71
+ load_remote_env_if_configured(override=True, logger_name="kapo.brain.remote_env")
72
+ config_path = os.path.join(os.path.dirname(__file__), "..", "config", "config.yaml")
73
+ with open(config_path, "r", encoding="utf-8") as handle:
74
+ raw = handle.read()
75
+
76
+ for key, value in os.environ.items():
77
+ raw = raw.replace(f"${{{key}}}", value)
78
+
79
+ parsed = yaml.safe_load(raw) or {}
80
+ CONFIG_CACHE = _normalize_config_paths(_strip_unresolved_placeholders(parsed))
81
+ return CONFIG_CACHE
82
+
83
+
84
+ def is_remote_brain_only() -> bool:
85
+ cfg = load_config()
86
+ value = cfg.get("REMOTE_BRAIN_ONLY", os.getenv("REMOTE_BRAIN_ONLY", "0"))
87
+ return str(value).strip().lower() in {"1", "true", "yes", "on"}
88
+
89
+
90
+ def setup_logging() -> None:
91
+ global _LOGGING_READY
92
+ if _LOGGING_READY:
93
+ return
94
+
95
+ log_cfg_path = os.path.join(os.path.dirname(__file__), "..", "config", "logging.yaml")
96
+ if not os.path.exists(log_cfg_path):
97
+ logging.basicConfig(level=logging.INFO)
98
+ _LOGGING_READY = True
99
+ return
100
+
101
+ try:
102
+ with open(log_cfg_path, "r", encoding="utf-8") as handle:
103
+ cfg = yaml.safe_load(handle) or {}
104
+ logging.config.dictConfig(cfg)
105
+ except Exception:
106
+ logging.basicConfig(level=logging.INFO)
107
+ logging.getLogger("kapo").warning("Falling back to basic logging configuration")
108
+
109
+ _LOGGING_READY = True
110
+
111
+
112
+ def get_logger(name: str) -> logging.Logger:
113
+ setup_logging()
114
+ return logging.getLogger(name)
115
+
116
+
117
+ def _normalize_base_url(candidate: Any) -> str:
118
+ text = "" if candidate is None else str(candidate).strip()
119
+ if not text:
120
+ return ""
121
+ if "://" not in text:
122
+ text = f"http://{text}"
123
+ return text.rstrip("/")
124
+
125
+
126
+ def get_executor_url(cfg: dict) -> str:
127
+ env_url = _normalize_base_url(os.getenv("EXECUTOR_URL"))
128
+ if env_url:
129
+ return env_url
130
+
131
+ cfg_url = _normalize_base_url(cfg.get("EXECUTOR_URL"))
132
+ if cfg_url:
133
+ return cfg_url
134
+
135
+ scheme = str(cfg.get("EXECUTOR_SCHEME") or os.getenv("EXECUTOR_SCHEME", "http")).strip() or "http"
136
+ host = str(cfg.get("EXECUTOR_HOST") or os.getenv("EXECUTOR_HOST", "localhost")).strip()
137
+ port = str(cfg.get("EXECUTOR_PORT") or os.getenv("EXECUTOR_PORT", "9000")).strip()
138
+
139
+ if "://" in host:
140
+ return host.rstrip("/")
141
+ if ":" in host:
142
+ return f"{scheme}://{host}".rstrip("/")
143
+ return f"{scheme}://{host}:{port}".rstrip("/")
144
+
145
+
146
+ def get_executor_headers(cfg: dict) -> dict:
147
+ header = cfg.get("EXECUTOR_BYPASS_HEADER") or os.getenv("EXECUTOR_BYPASS_HEADER")
148
+ value = cfg.get("EXECUTOR_BYPASS_VALUE") or os.getenv("EXECUTOR_BYPASS_VALUE")
149
+ if header and value:
150
+ return {str(header): str(value)}
151
+ return {}
152
+
153
+
154
+ def get_brain_headers(cfg: dict) -> dict:
155
+ header = cfg.get("BRAIN_BYPASS_HEADER") or os.getenv("BRAIN_BYPASS_HEADER")
156
+ value = cfg.get("BRAIN_BYPASS_VALUE") or os.getenv("BRAIN_BYPASS_VALUE")
157
+ if header and value:
158
+ return {str(header): str(value)}
159
+ return {}
brain_server/api/firebase_store.py ADDED
@@ -0,0 +1,356 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Shared-state store for brain runtime via Google Drive, local files, or Firebase."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ import os
8
+ import threading
9
+ import time
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from shared.google_drive_state import GoogleDriveStateClient
14
+
15
+
16
+ class FirebaseStore:
17
+ def __init__(self, component: str, logger_name: str = "kapo.brain.firebase") -> None:
18
+ self.component = component
19
+ self.logger = logging.getLogger(logger_name)
20
+ self._db = None
21
+ self._lock = threading.Lock()
22
+ self._read_cache: dict[str, tuple[float, Any]] = {}
23
+ self._list_cache: dict[str, tuple[float, list[dict[str, Any]]]] = {}
24
+ self._write_cache: dict[str, tuple[float, str]] = {}
25
+ self._quota_backoff_until: float = 0.0
26
+ self._drive = GoogleDriveStateClient(self.logger)
27
+
28
+ def backend(self) -> str:
29
+ configured = str(os.getenv("KAPO_SHARED_STATE_BACKEND", "")).strip().lower()
30
+ if configured in {"google_drive", "drive", "gdrive"}:
31
+ return "google_drive"
32
+ if configured in {"file", "files"}:
33
+ return "file"
34
+ if configured in {"firebase", "firestore"}:
35
+ return "firebase"
36
+ if configured in {"disabled", "off", "none"}:
37
+ return "disabled"
38
+ if self._drive.enabled():
39
+ return "google_drive"
40
+ if str(os.getenv("FIREBASE_ENABLED", "0")).strip().lower() in {"1", "true", "yes", "on"}:
41
+ return "firebase"
42
+ return "file"
43
+
44
+ def enabled(self) -> bool:
45
+ return self.backend() != "disabled"
46
+
47
+ def namespace(self) -> str:
48
+ return str(os.getenv("FIREBASE_NAMESPACE", "kapo")).strip() or "kapo"
49
+
50
+ def storage_root(self) -> Path:
51
+ configured = str(os.getenv("KAPO_SHARED_STATE_DIR", "")).strip()
52
+ if configured:
53
+ root = Path(configured).expanduser()
54
+ if not root.is_absolute():
55
+ root = Path.cwd().resolve() / root
56
+ else:
57
+ root = (Path.cwd().resolve() / "data" / "local" / "shared_state").resolve()
58
+ root.mkdir(parents=True, exist_ok=True)
59
+ return root
60
+
61
+ def _service_payload(self) -> dict[str, Any] | None:
62
+ raw = str(os.getenv("FIREBASE_SERVICE_ACCOUNT_JSON", "")).strip()
63
+ if not raw:
64
+ return None
65
+ try:
66
+ return json.loads(raw)
67
+ except Exception:
68
+ self.logger.exception("Invalid Firebase service account JSON")
69
+ return None
70
+
71
+ @staticmethod
72
+ def _service_path() -> str:
73
+ return str(os.getenv("FIREBASE_SERVICE_ACCOUNT_PATH", "")).strip()
74
+
75
+ def _client(self):
76
+ if self.backend() != "firebase":
77
+ return None
78
+ with self._lock:
79
+ if self._db is not None:
80
+ return self._db
81
+ try:
82
+ import firebase_admin
83
+ from firebase_admin import credentials, firestore
84
+
85
+ if not firebase_admin._apps:
86
+ payload = self._service_payload()
87
+ if payload:
88
+ cred = credentials.Certificate(payload)
89
+ else:
90
+ service_path = self._service_path()
91
+ if not service_path:
92
+ return None
93
+ path_obj = Path(service_path).expanduser()
94
+ if not path_obj.exists() or not path_obj.is_file():
95
+ self.logger.warning(
96
+ "Firebase service account path is unavailable on this runtime: %s",
97
+ service_path,
98
+ )
99
+ return None
100
+ cred = credentials.Certificate(str(path_obj.resolve()))
101
+ options = {}
102
+ project_id = str(os.getenv("FIREBASE_PROJECT_ID", "")).strip()
103
+ if project_id:
104
+ options["projectId"] = project_id
105
+ firebase_admin.initialize_app(cred, options or None)
106
+ self._db = firestore.client()
107
+ return self._db
108
+ except Exception:
109
+ self.logger.exception("Failed to initialize Firebase client")
110
+ return None
111
+
112
+ def _collection(self, name: str) -> str:
113
+ return f"{self.namespace()}_{name}"
114
+
115
+ @staticmethod
116
+ def _safe_id(value: str, default: str = "default") -> str:
117
+ text = str(value or "").strip() or default
118
+ return "".join(ch if ch.isalnum() or ch in {"-", "_", "."} else "_" for ch in text)[:180]
119
+
120
+ @staticmethod
121
+ def _payload_hash(payload: Any) -> str:
122
+ return json.dumps(payload, ensure_ascii=False, sort_keys=True, default=str)
123
+
124
+ @staticmethod
125
+ def _is_quota_error(exc: Exception) -> bool:
126
+ text = str(exc or "").lower()
127
+ return "resourceexhausted" in text or "quota exceeded" in text or "429" in text
128
+
129
+ def _quota_backoff_active(self) -> bool:
130
+ return time.time() < self._quota_backoff_until
131
+
132
+ def _activate_quota_backoff(self, seconds: float | None = None) -> None:
133
+ delay = float(seconds or os.getenv("FIREBASE_QUOTA_BACKOFF_SEC", "120") or 120)
134
+ self._quota_backoff_until = max(self._quota_backoff_until, time.time() + max(5.0, delay))
135
+
136
+ def _should_skip_write(self, key: str, payload: Any, min_interval_sec: float) -> bool:
137
+ now = time.time()
138
+ payload_hash = self._payload_hash(payload)
139
+ last = self._write_cache.get(key)
140
+ if last and last[1] == payload_hash and (now - last[0]) < min_interval_sec:
141
+ return True
142
+ self._write_cache[key] = (now, payload_hash)
143
+ return False
144
+
145
+ def _file_collection_dir(self, collection: str) -> Path:
146
+ path = self.storage_root() / self._safe_id(collection, "collection")
147
+ path.mkdir(parents=True, exist_ok=True)
148
+ return path
149
+
150
+ def _file_doc_path(self, collection: str, doc_id: str) -> Path:
151
+ return self._file_collection_dir(collection) / f"{self._safe_id(doc_id)}.json"
152
+
153
+ def _write_json_atomic(self, path: Path, payload: dict[str, Any]) -> None:
154
+ path.parent.mkdir(parents=True, exist_ok=True)
155
+ tmp = path.with_suffix(f"{path.suffix}.tmp")
156
+ tmp.write_text(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True), encoding="utf-8")
157
+ tmp.replace(path)
158
+
159
+ def get_document(self, collection: str, doc_id: str, ttl_sec: float = 12.0) -> dict[str, Any]:
160
+ safe_doc = self._safe_id(doc_id)
161
+ cache_key = f"{collection}:{safe_doc}"
162
+ now = time.time()
163
+ cached = self._read_cache.get(cache_key)
164
+ if cached and (now - cached[0]) < ttl_sec:
165
+ return dict(cached[1] or {})
166
+ backend = self.backend()
167
+ if backend == "google_drive":
168
+ payload = self._drive.get_document(collection, safe_doc)
169
+ self._read_cache[cache_key] = (now, payload)
170
+ return dict(payload or {})
171
+ if backend == "file":
172
+ path = self._file_doc_path(collection, safe_doc)
173
+ if not path.exists():
174
+ return {}
175
+ try:
176
+ payload = json.loads(path.read_text(encoding="utf-8"))
177
+ except Exception:
178
+ self.logger.warning("Failed to read shared-state file %s", path, exc_info=True)
179
+ return {}
180
+ self._read_cache[cache_key] = (now, payload)
181
+ return dict(payload or {})
182
+ if self._quota_backoff_active():
183
+ return dict(cached[1] or {}) if cached else {}
184
+ db = self._client()
185
+ if db is None:
186
+ return dict(cached[1] or {}) if cached else {}
187
+ try:
188
+ snapshot = db.collection(self._collection(collection)).document(safe_doc).get()
189
+ payload = snapshot.to_dict() if snapshot.exists else {}
190
+ self._read_cache[cache_key] = (now, payload)
191
+ return dict(payload or {})
192
+ except Exception as exc:
193
+ if self._is_quota_error(exc):
194
+ self._activate_quota_backoff()
195
+ self.logger.warning("Firebase quota exceeded while reading %s/%s; using cache/backoff", collection, safe_doc)
196
+ else:
197
+ self.logger.exception("Failed to read Firebase document %s/%s", collection, safe_doc)
198
+ return dict(cached[1] or {}) if cached else {}
199
+
200
+ def set_document(self, collection: str, doc_id: str, payload: dict[str, Any], merge: bool = True, min_interval_sec: float = 5.0) -> bool:
201
+ safe_doc = self._safe_id(doc_id)
202
+ cache_key = f"{collection}:{safe_doc}"
203
+ body = dict(payload or {})
204
+ body["component"] = self.component
205
+ body["updated_at"] = time.time()
206
+ if self._should_skip_write(cache_key, body, min_interval_sec):
207
+ return True
208
+ backend = self.backend()
209
+ if backend == "google_drive":
210
+ stored = self._drive.set_document(collection, safe_doc, body, merge=merge)
211
+ if stored:
212
+ self._read_cache.pop(cache_key, None)
213
+ stale_prefix = f"{collection}:list:"
214
+ for key in list(self._list_cache.keys()):
215
+ if key.startswith(stale_prefix):
216
+ self._list_cache.pop(key, None)
217
+ return stored
218
+ if backend == "file":
219
+ try:
220
+ path = self._file_doc_path(collection, safe_doc)
221
+ existing = {}
222
+ if merge and path.exists():
223
+ existing = json.loads(path.read_text(encoding="utf-8"))
224
+ combined = {**existing, **body} if merge else body
225
+ combined.setdefault("id", safe_doc)
226
+ self._write_json_atomic(path, combined)
227
+ self._read_cache.pop(cache_key, None)
228
+ stale_prefix = f"{collection}:list:"
229
+ for key in list(self._list_cache.keys()):
230
+ if key.startswith(stale_prefix):
231
+ self._list_cache.pop(key, None)
232
+ return True
233
+ except Exception:
234
+ self.logger.warning("Failed to write shared-state file %s/%s", collection, safe_doc, exc_info=True)
235
+ return False
236
+ if self._quota_backoff_active():
237
+ return False
238
+ db = self._client()
239
+ if db is None:
240
+ return False
241
+ try:
242
+ if self._should_skip_write(cache_key, body, min_interval_sec):
243
+ return True
244
+ db.collection(self._collection(collection)).document(safe_doc).set(body, merge=merge)
245
+ self._read_cache.pop(cache_key, None)
246
+ stale_prefix = f"{collection}:list:"
247
+ for key in list(self._list_cache.keys()):
248
+ if key.startswith(stale_prefix):
249
+ self._list_cache.pop(key, None)
250
+ return True
251
+ except Exception as exc:
252
+ if self._is_quota_error(exc):
253
+ self._activate_quota_backoff()
254
+ self.logger.warning("Firebase quota exceeded while writing %s/%s; write skipped", collection, safe_doc)
255
+ else:
256
+ self.logger.exception("Failed to write Firebase document %s/%s", collection, safe_doc)
257
+ return False
258
+
259
+ def list_documents(self, collection: str, limit: int = 200, ttl_sec: float = 30.0) -> list[dict[str, Any]]:
260
+ cache_key = f"{collection}:list:{max(1, int(limit))}"
261
+ now = time.time()
262
+ cached = self._list_cache.get(cache_key)
263
+ if cached and (now - cached[0]) < ttl_sec:
264
+ return [dict(item) for item in (cached[1] or [])]
265
+ backend = self.backend()
266
+ if backend == "google_drive":
267
+ items = self._drive.list_documents(collection, limit=max(1, int(limit)))
268
+ self._list_cache[cache_key] = (now, [dict(item) for item in items])
269
+ return items
270
+ if backend == "file":
271
+ items: list[dict[str, Any]] = []
272
+ try:
273
+ paths = sorted(
274
+ self._file_collection_dir(collection).glob("*.json"),
275
+ key=lambda path: path.stat().st_mtime,
276
+ reverse=True,
277
+ )
278
+ for path in paths[: max(1, int(limit))]:
279
+ payload = json.loads(path.read_text(encoding="utf-8"))
280
+ payload.setdefault("id", path.stem)
281
+ items.append(payload)
282
+ except Exception:
283
+ self.logger.warning("Failed to list shared-state files for %s", collection, exc_info=True)
284
+ self._list_cache[cache_key] = (now, [dict(item) for item in items])
285
+ return items
286
+ if self._quota_backoff_active():
287
+ return [dict(item) for item in ((cached[1] if cached else []) or [])]
288
+ db = self._client()
289
+ if db is None:
290
+ return [dict(item) for item in ((cached[1] if cached else []) or [])]
291
+ try:
292
+ docs = db.collection(self._collection(collection)).limit(max(1, int(limit))).stream()
293
+ items: list[dict[str, Any]] = []
294
+ for doc in docs:
295
+ payload = doc.to_dict() or {}
296
+ payload.setdefault("id", doc.id)
297
+ items.append(payload)
298
+ self._list_cache[cache_key] = (now, [dict(item) for item in items])
299
+ return items
300
+ except Exception as exc:
301
+ if self._is_quota_error(exc):
302
+ self._activate_quota_backoff()
303
+ self.logger.warning("Firebase quota exceeded while listing %s; using cache/backoff", collection)
304
+ else:
305
+ self.logger.exception("Failed to list Firebase collection %s", collection)
306
+ return [dict(item) for item in ((cached[1] if cached else []) or [])]
307
+
308
+ def delete_document(self, collection: str, doc_id: str) -> bool:
309
+ safe_doc = self._safe_id(doc_id)
310
+ backend = self.backend()
311
+ if backend == "google_drive":
312
+ deleted = self._drive.delete_document(collection, safe_doc)
313
+ if deleted:
314
+ self._read_cache.pop(f"{collection}:{safe_doc}", None)
315
+ self._write_cache.pop(f"{collection}:{safe_doc}", None)
316
+ stale_prefix = f"{collection}:list:"
317
+ for key in list(self._list_cache.keys()):
318
+ if key.startswith(stale_prefix):
319
+ self._list_cache.pop(key, None)
320
+ return deleted
321
+ if backend == "file":
322
+ try:
323
+ path = self._file_doc_path(collection, safe_doc)
324
+ if path.exists():
325
+ path.unlink()
326
+ self._read_cache.pop(f"{collection}:{safe_doc}", None)
327
+ self._write_cache.pop(f"{collection}:{safe_doc}", None)
328
+ stale_prefix = f"{collection}:list:"
329
+ for key in list(self._list_cache.keys()):
330
+ if key.startswith(stale_prefix):
331
+ self._list_cache.pop(key, None)
332
+ return True
333
+ except Exception:
334
+ self.logger.warning("Failed to delete shared-state file %s/%s", collection, safe_doc, exc_info=True)
335
+ return False
336
+ if self._quota_backoff_active():
337
+ return False
338
+ db = self._client()
339
+ if db is None:
340
+ return False
341
+ try:
342
+ db.collection(self._collection(collection)).document(safe_doc).delete()
343
+ self._read_cache.pop(f"{collection}:{safe_doc}", None)
344
+ self._write_cache.pop(f"{collection}:{safe_doc}", None)
345
+ stale_prefix = f"{collection}:list:"
346
+ for key in list(self._list_cache.keys()):
347
+ if key.startswith(stale_prefix):
348
+ self._list_cache.pop(key, None)
349
+ return True
350
+ except Exception as exc:
351
+ if self._is_quota_error(exc):
352
+ self._activate_quota_backoff()
353
+ self.logger.warning("Firebase quota exceeded while deleting %s/%s; delete skipped", collection, safe_doc)
354
+ else:
355
+ self.logger.exception("Failed to delete Firebase document %s/%s", collection, safe_doc)
356
+ return False
brain_server/api/main.py ADDED
@@ -0,0 +1,2356 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FastAPI entrypoint for the Brain Server."""
2
+ import base64
3
+ import gc
4
+ import logging
5
+ import os
6
+ import re
7
+ import shutil
8
+ import subprocess
9
+ import sys
10
+ import tempfile
11
+ import threading
12
+ import time
13
+ import zipfile
14
+ from collections import deque
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ import requests
19
+ from fastapi import FastAPI, File, UploadFile
20
+ from pydantic import BaseModel
21
+
22
+ from agents.memory_agent import MemoryAgent
23
+ from agents.planner_agent import PlannerAgent
24
+ from agents.reasoning_agent import ReasoningAgent
25
+ try:
26
+ from api import deps as deps_module
27
+ from api.deps import get_executor_headers, get_logger, load_config
28
+ from api.firebase_store import FirebaseStore
29
+ from api.routes_analyze import router as analyze_router
30
+ from api.routes_execute import router as execute_router
31
+ from api.routes_plan import router as plan_router
32
+ from shared.google_drive_state import GoogleDriveStateClient
33
+ except ImportError:
34
+ from . import deps as deps_module
35
+ from .deps import get_executor_headers, get_logger, load_config
36
+ from .firebase_store import FirebaseStore
37
+ from .routes_analyze import router as analyze_router
38
+ from .routes_execute import router as execute_router
39
+ from .routes_plan import router as plan_router
40
+ from shared.google_drive_state import GoogleDriveStateClient
41
+
42
+ logger = get_logger("kapo.brain.main")
43
+
44
+
45
+ def _configure_windows_utf8() -> None:
46
+ if os.name != "nt":
47
+ return
48
+ os.environ.setdefault("PYTHONUTF8", "1")
49
+ os.environ.setdefault("PYTHONIOENCODING", "utf-8")
50
+ os.environ.setdefault("PYTHONLEGACYWINDOWSSTDIO", "utf-8")
51
+ try:
52
+ import ctypes
53
+
54
+ kernel32 = ctypes.windll.kernel32
55
+ kernel32.SetConsoleCP(65001)
56
+ kernel32.SetConsoleOutputCP(65001)
57
+ except Exception:
58
+ pass
59
+
60
+
61
+ _configure_windows_utf8()
62
+
63
+ if hasattr(sys.stdout, "reconfigure"):
64
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
65
+ if hasattr(sys.stderr, "reconfigure"):
66
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace")
67
+
68
+ app = FastAPI(title="KAPO-AI Brain Server", version="1.0.0")
69
+ app.include_router(plan_router)
70
+ app.include_router(execute_router)
71
+ app.include_router(analyze_router)
72
+
73
+ MODEL = None
74
+ MODEL_ERROR = None
75
+ MODEL_META = {"repo_id": None, "filename": None, "path": None}
76
+ EMBED_MODEL = None
77
+ FIREBASE = FirebaseStore("brain", logger_name="kapo.brain.firebase")
78
+ DRIVE_STATE = GoogleDriveStateClient(logger)
79
+ FIREBASE_RUNTIME_CACHE: dict[str, tuple[float, Any]] = {}
80
+ RUNTIME_LOG_BUFFER: deque[dict[str, Any]] = deque(maxlen=200)
81
+ LAST_BRAIN_URL_REPORT: dict[str, Any] = {"url": "", "ts": 0.0}
82
+ RUNTIME_STATE_THREAD_STARTED = False
83
+
84
+ DEFAULT_MODEL_REPO = "QuantFactory/aya-expanse-8b-GGUF"
85
+ DEFAULT_MODEL_FILE = "aya-expanse-8b.Q4_K_M.gguf"
86
+ DEFAULT_MODEL_PROFILE_ID = "supervisor-ar-en-default"
87
+ HAS_MULTIPART = True
88
+ try:
89
+ import multipart # noqa: F401
90
+ except Exception:
91
+ HAS_MULTIPART = False
92
+
93
+
94
+ class RuntimeLogHandler(logging.Handler):
95
+ def emit(self, record) -> None:
96
+ try:
97
+ RUNTIME_LOG_BUFFER.append(
98
+ {
99
+ "ts": time.time(),
100
+ "level": record.levelname,
101
+ "name": record.name,
102
+ "message": record.getMessage(),
103
+ }
104
+ )
105
+ except Exception:
106
+ pass
107
+
108
+
109
+ _runtime_log_handler = RuntimeLogHandler(level=logging.WARNING)
110
+ if not any(isinstance(handler, RuntimeLogHandler) for handler in logger.handlers):
111
+ logger.addHandler(_runtime_log_handler)
112
+
113
+
114
+ def _feature_enabled(name: str, default: bool = False) -> bool:
115
+ value = os.getenv(name)
116
+ if value is None or str(value).strip() == "":
117
+ return default
118
+ return str(value).strip().lower() in {"1", "true", "yes", "on"}
119
+
120
+
121
+ def _remote_brain_only() -> bool:
122
+ return _feature_enabled("REMOTE_BRAIN_ONLY", default=False)
123
+
124
+
125
+ def _ngrok_bootstrap_enabled() -> bool:
126
+ return _feature_enabled("BRAIN_AUTO_NGROK", default=True)
127
+
128
+
129
+ def _configured_public_url() -> str:
130
+ return str(os.getenv("BRAIN_PUBLIC_URL", "")).strip().rstrip("/")
131
+
132
+
133
+ def _prefer_configured_public_url() -> bool:
134
+ provider = str(os.getenv("BRAIN_PROVIDER", "") or os.getenv("BRAIN_TEMPLATE", "")).strip().lower()
135
+ if "huggingface" in provider or "hf-space" in provider:
136
+ return True
137
+ return _feature_enabled("BRAIN_FORCE_CONFIGURED_PUBLIC_URL", default=False)
138
+
139
+
140
+ def _reuse_public_url_on_restart() -> bool:
141
+ return _feature_enabled("BRAIN_REUSE_PUBLIC_URL_ON_RESTART", default=True)
142
+
143
+
144
+ def _auto_publish_public_url_on_startup() -> bool:
145
+ return _feature_enabled("BRAIN_AUTO_PUBLISH_URL_ON_STARTUP", default=True)
146
+
147
+
148
+ def _internal_restart_in_progress() -> bool:
149
+ return _feature_enabled("KAPO_INTERNAL_RESTART", default=False)
150
+
151
+
152
+ def _fast_restart_enabled() -> bool:
153
+ return _feature_enabled("KAPO_FAST_INTERNAL_RESTART", default=True)
154
+
155
+
156
+ def _executor_connect_timeout() -> float:
157
+ return float(os.getenv("EXECUTOR_CONNECT_TIMEOUT_SEC", "3.0") or 3.0)
158
+
159
+
160
+ def _executor_read_timeout(name: str, default: float) -> float:
161
+ return float(os.getenv(name, str(default)) or default)
162
+
163
+
164
+ def _executor_roundtrip_allowed(feature_name: str, default: bool = True) -> bool:
165
+ executor_url = os.getenv("EXECUTOR_URL", "").strip()
166
+ if not executor_url:
167
+ return False
168
+ kaggle_defaults = {
169
+ "BRAIN_REMOTE_TRACE_STORE_ENABLED": False,
170
+ "BRAIN_REMOTE_AUTO_INGEST_ENABLED": False,
171
+ "BRAIN_REMOTE_STYLE_PROFILE_ENABLED": False,
172
+ }
173
+ effective_default = kaggle_defaults.get(feature_name, default) if _is_kaggle_runtime() else default
174
+ return _feature_enabled(feature_name, default=effective_default)
175
+
176
+
177
+ def _remote_runtime_reads_enabled() -> bool:
178
+ return _feature_enabled("KAPO_REMOTE_STATE_READS", default=False)
179
+
180
+
181
+ def _shared_state_backend() -> str:
182
+ return str(os.getenv("KAPO_SHARED_STATE_BACKEND", "")).strip().lower()
183
+
184
+
185
+ def _drive_bootstrap_configured() -> bool:
186
+ return bool(
187
+ str(os.getenv("GOOGLE_DRIVE_BOOTSTRAP_URL", "") or os.getenv("KAPO_BOOTSTRAP_URL", "") or "").strip()
188
+ )
189
+
190
+
191
+ def _bootstrap_shared_state() -> None:
192
+ if _drive_bootstrap_configured() or _shared_state_backend() in {"google_drive", "drive", "gdrive"}:
193
+ DRIVE_STATE.ensure_bootstrap_loaded(force=False)
194
+
195
+
196
+ def _startup_self_update_enabled() -> bool:
197
+ return _feature_enabled("KAPO_STARTUP_SELF_UPDATE", default=True)
198
+
199
+
200
+ def _should_report_brain_url(public_url: str) -> bool:
201
+ normalized = str(public_url or "").strip().rstrip("/")
202
+ if not normalized:
203
+ return False
204
+ interval_sec = max(30.0, float(os.getenv("BRAIN_REPORT_MIN_INTERVAL_SEC", "600") or 600))
205
+ previous_url = str(LAST_BRAIN_URL_REPORT.get("url") or "").strip()
206
+ previous_ts = float(LAST_BRAIN_URL_REPORT.get("ts") or 0.0)
207
+ now = time.time()
208
+ if normalized != previous_url or (now - previous_ts) >= interval_sec:
209
+ LAST_BRAIN_URL_REPORT["url"] = normalized
210
+ LAST_BRAIN_URL_REPORT["ts"] = now
211
+ return True
212
+ return False
213
+
214
+
215
+ def _download_model(repo_id: str, filename: str, hf_token: str | None = None) -> str:
216
+ from huggingface_hub import hf_hub_download
217
+
218
+ configured_cache = str(os.getenv("MODEL_CACHE_DIR", "") or "").strip()
219
+ if configured_cache:
220
+ cache_dir = configured_cache
221
+ elif _is_kaggle_runtime():
222
+ cache_dir = str((_project_root() / "models_cache").resolve())
223
+ else:
224
+ cache_dir = os.path.join(tempfile.gettempdir(), "kapo_models")
225
+ os.makedirs(cache_dir, exist_ok=True)
226
+ return hf_hub_download(repo_id=repo_id, filename=filename, cache_dir=cache_dir, token=hf_token)
227
+
228
+
229
+ def ensure_model_loaded(repo_id: str, filename: str, hf_token: str | None = None) -> None:
230
+ global MODEL, MODEL_ERROR, MODEL_META
231
+ repo_id = (repo_id or "").strip()
232
+ filename = (filename or "").strip()
233
+ if not repo_id or not filename:
234
+ MODEL = None
235
+ MODEL_ERROR = "model repo/file missing"
236
+ return
237
+
238
+ try:
239
+ model_path = _download_model(repo_id, filename, hf_token=hf_token)
240
+ except Exception as exc:
241
+ MODEL = None
242
+ MODEL_ERROR = f"model download failed: {exc}"
243
+ logger.exception("Model download failed")
244
+ return
245
+
246
+ try:
247
+ from llama_cpp import Llama
248
+
249
+ MODEL = Llama(model_path=model_path, n_ctx=4096)
250
+ MODEL_ERROR = None
251
+ MODEL_META = {"repo_id": repo_id, "filename": filename, "path": model_path}
252
+ logger.info("Loaded model %s/%s", repo_id, filename)
253
+ except Exception as exc:
254
+ MODEL = None
255
+ MODEL_ERROR = f"model load failed: {exc}"
256
+ logger.exception("Model load failed")
257
+
258
+
259
+ def _load_embed_model() -> None:
260
+ global EMBED_MODEL
261
+ if EMBED_MODEL is not None:
262
+ return
263
+
264
+ from sentence_transformers import SentenceTransformer
265
+
266
+ model_name = os.getenv("EMBED_MODEL", "sentence-transformers/all-MiniLM-L6-v2")
267
+ EMBED_MODEL = SentenceTransformer(model_name)
268
+ logger.info("Loaded embedding model %s", model_name)
269
+
270
+
271
+ def _load_default_model() -> None:
272
+ repo_id = os.getenv("MODEL_REPO", DEFAULT_MODEL_REPO)
273
+ filename = os.getenv("MODEL_FILE", DEFAULT_MODEL_FILE)
274
+ ensure_model_loaded(repo_id, filename, hf_token=os.getenv("HF_TOKEN"))
275
+
276
+
277
+ def _brain_headers() -> dict:
278
+ cfg = load_config()
279
+ return get_executor_headers(cfg)
280
+
281
+
282
+ def _project_root() -> Path:
283
+ return Path(__file__).resolve().parents[1]
284
+
285
+
286
+ def _is_kaggle_runtime() -> bool:
287
+ return "/kaggle/" in str(_project_root()).replace("\\", "/") or bool(os.getenv("KAGGLE_KERNEL_RUN_TYPE"))
288
+
289
+
290
+ def _is_hf_space_runtime() -> bool:
291
+ return str(os.getenv("HF_SPACE_DOCKER", "0")).strip().lower() in {"1", "true", "yes", "on"} or bool(os.getenv("SPACE_ID"))
292
+
293
+
294
+ def _apply_executor_settings(settings: dict[str, Any]) -> None:
295
+ for key in (
296
+ "NGROK_AUTHTOKEN",
297
+ "MODEL_REPO",
298
+ "MODEL_FILE",
299
+ "MODEL_PROFILE_ID",
300
+ "SUPERVISOR_MODEL_PROFILE_ID",
301
+ "EMBED_MODEL",
302
+ "REQUEST_TIMEOUT_SEC",
303
+ "REQUEST_RETRIES",
304
+ "CHAT_TIMEOUT_SEC",
305
+ "EXECUTOR_BYPASS_HEADER",
306
+ "EXECUTOR_BYPASS_VALUE",
307
+ "BRAIN_BYPASS_HEADER",
308
+ "BRAIN_BYPASS_VALUE",
309
+ "REMOTE_BRAIN_ONLY",
310
+ "KAGGLE_AUTO_BOOTSTRAP",
311
+ "BRAIN_AUTO_NGROK",
312
+ "BRAIN_AUTO_PUBLISH_URL_ON_STARTUP",
313
+ "BRAIN_PUBLIC_URL",
314
+ "BRAIN_REUSE_PUBLIC_URL_ON_RESTART",
315
+ "KAGGLE_SYNC_SUBDIR",
316
+ "BRAIN_ROLES",
317
+ "BRAIN_LANGUAGES",
318
+ "BRAIN_REMOTE_KNOWLEDGE_ENABLED",
319
+ "BRAIN_REMOTE_WEB_SEARCH_ENABLED",
320
+ "BRAIN_REMOTE_TRACE_STORE_ENABLED",
321
+ "BRAIN_REMOTE_AUTO_INGEST_ENABLED",
322
+ "BRAIN_LOCAL_RAG_FALLBACK_ENABLED",
323
+ "EXECUTOR_CONNECT_TIMEOUT_SEC",
324
+ "BRAIN_REMOTE_KNOWLEDGE_TIMEOUT_SEC",
325
+ "BRAIN_REMOTE_WEB_SEARCH_TIMEOUT_SEC",
326
+ "BRAIN_REMOTE_TRACE_STORE_TIMEOUT_SEC",
327
+ "BRAIN_REMOTE_AUTO_INGEST_TIMEOUT_SEC",
328
+ "FIREBASE_ENABLED",
329
+ "FIREBASE_PROJECT_ID",
330
+ "FIREBASE_SERVICE_ACCOUNT_PATH",
331
+ "FIREBASE_SERVICE_ACCOUNT_JSON",
332
+ "FIREBASE_NAMESPACE",
333
+ "KAPO_SHARED_STATE_BACKEND",
334
+ "GOOGLE_DRIVE_SHARED_STATE_FOLDER_ID",
335
+ "GOOGLE_DRIVE_SHARED_STATE_PREFIX",
336
+ "GOOGLE_DRIVE_BOOTSTRAP_URL",
337
+ "KAPO_BOOTSTRAP_URL",
338
+ "GOOGLE_DRIVE_ACCESS_TOKEN",
339
+ "GOOGLE_DRIVE_REFRESH_TOKEN",
340
+ "GOOGLE_DRIVE_CLIENT_SECRET_JSON",
341
+ "GOOGLE_DRIVE_CLIENT_SECRET_JSON_BASE64",
342
+ "GOOGLE_DRIVE_CLIENT_SECRET_PATH",
343
+ "GOOGLE_DRIVE_TOKEN_EXPIRES_AT",
344
+ "KAPO_CONTROL_PLANE_URL",
345
+ "KAPO_CLOUDFLARE_QUEUE_NAME",
346
+ ):
347
+ value = settings.get(key)
348
+ if value not in (None, ""):
349
+ os.environ[key] = str(value)
350
+ deps_module.CONFIG_CACHE = None
351
+
352
+
353
+ def _apply_firebase_runtime_settings() -> None:
354
+ if not _remote_runtime_reads_enabled():
355
+ return
356
+ if not FIREBASE.enabled():
357
+ return
358
+ shared = FIREBASE.get_document("settings", "global")
359
+ runtime = FIREBASE.get_document("runtime", "executor")
360
+ role_items = FIREBASE.list_documents("roles", limit=64)
361
+ merged = {}
362
+ merged.update(shared or {})
363
+ merged.update(runtime or {})
364
+ mappings = {
365
+ "executor_public_url": "EXECUTOR_PUBLIC_URL",
366
+ "executor_url": "EXECUTOR_URL",
367
+ "model_repo": "MODEL_REPO",
368
+ "model_file": "MODEL_FILE",
369
+ "model_profile_id": "MODEL_PROFILE_ID",
370
+ "supervisor_model_profile_id": "SUPERVISOR_MODEL_PROFILE_ID",
371
+ "brain_roles": "BRAIN_ROLES",
372
+ "brain_languages": "BRAIN_LANGUAGES",
373
+ }
374
+ current_brain_url = str(merged.get("current_brain_url") or "").strip()
375
+ if current_brain_url:
376
+ os.environ["KAPO_EXECUTOR_CURRENT_BRAIN_URL"] = current_brain_url
377
+ for key, env_name in mappings.items():
378
+ value = merged.get(key)
379
+ if value not in (None, ""):
380
+ os.environ[env_name] = str(value)
381
+ if role_items:
382
+ enabled_roles = [
383
+ str(item.get("name") or item.get("id") or "").strip().lower()
384
+ for item in role_items
385
+ if str(item.get("enabled", True)).strip().lower() not in {"0", "false", "no", "off"}
386
+ ]
387
+ enabled_roles = [role for role in enabled_roles if role]
388
+ if enabled_roles:
389
+ os.environ["BRAIN_ROLES"] = ",".join(dict.fromkeys(enabled_roles))
390
+ deps_module.CONFIG_CACHE = None
391
+
392
+
393
+ def _firebase_collection_cache_key(name: str) -> str:
394
+ return f"collection:{name}"
395
+
396
+
397
+ def _firebase_list_documents_cached(collection: str, ttl_sec: float = 30.0, limit: int = 200) -> list[dict[str, Any]]:
398
+ if not _remote_runtime_reads_enabled():
399
+ return []
400
+ if not FIREBASE.enabled():
401
+ return []
402
+ key = _firebase_collection_cache_key(collection)
403
+ now = time.time()
404
+ cached = FIREBASE_RUNTIME_CACHE.get(key)
405
+ if cached and (now - cached[0]) < ttl_sec:
406
+ return list(cached[1] or [])
407
+ items = FIREBASE.list_documents(collection, limit=limit)
408
+ FIREBASE_RUNTIME_CACHE[key] = (now, items)
409
+ return list(items or [])
410
+
411
+
412
+ def _firebase_role_profiles() -> list[dict[str, Any]]:
413
+ items = _firebase_list_documents_cached("roles", ttl_sec=30.0, limit=64)
414
+ if items:
415
+ return items
416
+ roles = [part.strip() for part in str(os.getenv("BRAIN_ROLES", "")).split(",") if part.strip()]
417
+ return [{"name": role, "enabled": True} for role in roles]
418
+
419
+
420
+ def _firebase_runtime_snapshot() -> dict[str, Any]:
421
+ return {
422
+ "platforms": _firebase_list_documents_cached("platforms", ttl_sec=45.0, limit=64),
423
+ "models": _firebase_list_documents_cached("models", ttl_sec=45.0, limit=128),
424
+ "prompts": _firebase_list_documents_cached("prompts", ttl_sec=20.0, limit=128),
425
+ "roles": _firebase_role_profiles(),
426
+ }
427
+
428
+
429
+ def _json_safe(value: Any) -> Any:
430
+ if isinstance(value, dict):
431
+ return {str(key): _json_safe(item) for key, item in value.items()}
432
+ if isinstance(value, list):
433
+ return [_json_safe(item) for item in value]
434
+ if isinstance(value, tuple):
435
+ return [_json_safe(item) for item in value]
436
+ if isinstance(value, (str, int, float, bool)) or value is None:
437
+ return value
438
+ return str(value)
439
+
440
+
441
+ def _firebase_prompt_body(role_name: str, language: str = "en") -> str:
442
+ role_name = str(role_name or "").strip().lower()
443
+ language = str(language or "en").strip().lower()
444
+ if not role_name:
445
+ return ""
446
+ prompts = _firebase_runtime_snapshot().get("prompts", [])
447
+ exact = []
448
+ fallback = []
449
+ for item in prompts:
450
+ if str(item.get("role_name") or "").strip().lower() != role_name:
451
+ continue
452
+ if str(item.get("enabled", True)).strip().lower() in {"0", "false", "no", "off"}:
453
+ continue
454
+ item_lang = str(item.get("language") or "en").strip().lower()
455
+ body = str(item.get("body") or "").strip()
456
+ if not body:
457
+ continue
458
+ if item_lang == language:
459
+ exact.append(body)
460
+ elif item_lang == "en":
461
+ fallback.append(body)
462
+ if exact:
463
+ return exact[0]
464
+ if fallback:
465
+ return fallback[0]
466
+ return ""
467
+
468
+
469
+ def _prepare_runtime_environment() -> None:
470
+ try:
471
+ _bootstrap_shared_state()
472
+ except Exception:
473
+ logger.warning("Shared-state bootstrap preload failed", exc_info=True)
474
+ if not _is_kaggle_runtime():
475
+ return
476
+
477
+ source_root = _project_root()
478
+ source_text = str(source_root).replace("\\", "/")
479
+ runtime_root_env = os.getenv("KAPO_RUNTIME_ROOT", "").strip()
480
+ if runtime_root_env:
481
+ runtime_root = Path(runtime_root_env).resolve()
482
+ elif source_text.startswith("/kaggle/working/"):
483
+ runtime_root = source_root.resolve()
484
+ else:
485
+ runtime_root = Path("/kaggle/working/KAPO-AI-SYSTEM").resolve()
486
+ sync_root = runtime_root
487
+ auto_bootstrap = str(os.getenv("KAGGLE_AUTO_BOOTSTRAP", "1")).strip().lower() in {"1", "true", "yes", "on"}
488
+
489
+ if auto_bootstrap and source_text.startswith("/kaggle/input/") and source_root != runtime_root:
490
+ if runtime_root.exists():
491
+ shutil.rmtree(runtime_root, ignore_errors=True)
492
+ shutil.copytree(
493
+ source_root,
494
+ runtime_root,
495
+ ignore=shutil.ignore_patterns("__pycache__", "*.pyc", ".git", ".venv"),
496
+ )
497
+ if str(runtime_root) not in sys.path:
498
+ sys.path.insert(0, str(runtime_root))
499
+
500
+ data_dir = runtime_root / "data" / "local" / "brain_runtime"
501
+ data_dir.mkdir(parents=True, exist_ok=True)
502
+ os.environ["KAPO_RUNTIME_ROOT"] = str(runtime_root)
503
+ os.environ["KAPO_SYNC_ROOT"] = str(sync_root)
504
+ os.environ["LOCAL_DATA_DIR"] = str(data_dir)
505
+ os.environ["DB_PATH"] = str(data_dir / "episodic.db")
506
+ os.environ["TOOLS_DB_PATH"] = str(data_dir / "tools.db")
507
+ os.environ["FAISS_INDEX_PATH"] = str(data_dir / "faiss.index")
508
+ os.environ["REMOTE_BRAIN_ONLY"] = str(os.getenv("REMOTE_BRAIN_ONLY", "1") or "1")
509
+ saved_executor_url = _load_saved_executor_url()
510
+ current_executor_url = str(os.getenv("EXECUTOR_URL", "") or "").strip()
511
+ if saved_executor_url and not current_executor_url:
512
+ os.environ["EXECUTOR_URL"] = saved_executor_url
513
+ deps_module.CONFIG_CACHE = None
514
+
515
+
516
+ def _sync_target_root() -> str:
517
+ return os.getenv("KAPO_SYNC_ROOT") or os.getenv("KAPO_RUNTIME_ROOT") or os.getcwd()
518
+
519
+
520
+ def _sync_root_path() -> Path:
521
+ return Path(_sync_target_root()).resolve()
522
+
523
+
524
+ def _resolve_sync_path(user_path: str | None = None) -> Path:
525
+ root = _sync_root_path()
526
+ relative = str(user_path or "").strip().replace("\\", "/").lstrip("/")
527
+ candidate = (root / relative).resolve() if relative else root
528
+ if candidate != root and root not in candidate.parents:
529
+ raise ValueError("Path escapes sync root")
530
+ return candidate
531
+
532
+
533
+ def _describe_sync_entry(path: Path) -> dict[str, Any]:
534
+ stat = path.stat()
535
+ return {
536
+ "name": path.name or str(path),
537
+ "path": str(path.relative_to(_sync_root_path())).replace("\\", "/") if path != _sync_root_path() else "",
538
+ "is_dir": path.is_dir(),
539
+ "size": stat.st_size,
540
+ "modified_at": stat.st_mtime,
541
+ }
542
+
543
+
544
+ def _public_url_state_path() -> Path:
545
+ runtime_root = Path(_sync_target_root()).resolve()
546
+ state_dir = runtime_root / "data" / "local" / "brain_runtime"
547
+ state_dir.mkdir(parents=True, exist_ok=True)
548
+ return state_dir / "public_url.txt"
549
+
550
+
551
+ def _remember_public_url(public_url: str) -> None:
552
+ value = str(public_url or "").strip().rstrip("/")
553
+ if not value:
554
+ return
555
+ os.environ["BRAIN_PUBLIC_URL"] = value
556
+ try:
557
+ _public_url_state_path().write_text(value, encoding="utf-8")
558
+ except Exception:
559
+ logger.warning("Failed to persist public URL", exc_info=True)
560
+
561
+
562
+ def _load_saved_public_url() -> str:
563
+ try:
564
+ value = _public_url_state_path().read_text(encoding="utf-8").strip().rstrip("/")
565
+ return value
566
+ except Exception:
567
+ return ""
568
+
569
+
570
+ def _ngrok_api_state_path() -> Path:
571
+ runtime_root = Path(_sync_target_root()).resolve()
572
+ state_dir = runtime_root / "data" / "local" / "brain_runtime"
573
+ state_dir.mkdir(parents=True, exist_ok=True)
574
+ return state_dir / "ngrok_api_url.txt"
575
+
576
+
577
+ def _executor_url_state_path() -> Path:
578
+ runtime_root = Path(_sync_target_root()).resolve()
579
+ state_dir = runtime_root / "data" / "local" / "brain_runtime"
580
+ state_dir.mkdir(parents=True, exist_ok=True)
581
+ return state_dir / "executor_url.txt"
582
+
583
+
584
+ def _remember_executor_url(executor_url: str) -> None:
585
+ value = str(executor_url or "").strip().rstrip("/")
586
+ if not value:
587
+ return
588
+ os.environ["EXECUTOR_URL"] = value
589
+ try:
590
+ _executor_url_state_path().write_text(value, encoding="utf-8")
591
+ except Exception:
592
+ logger.warning("Failed to persist executor URL", exc_info=True)
593
+
594
+
595
+ def _load_saved_executor_url() -> str:
596
+ configured = str(os.getenv("EXECUTOR_URL", "")).strip().rstrip("/")
597
+ if configured:
598
+ return configured
599
+ try:
600
+ return _executor_url_state_path().read_text(encoding="utf-8").strip().rstrip("/")
601
+ except Exception:
602
+ return ""
603
+
604
+
605
+ def _brain_state_id() -> str:
606
+ public_url = str(os.getenv("BRAIN_PUBLIC_URL") or _load_saved_public_url() or "").strip().rstrip("/")
607
+ if public_url:
608
+ return re.sub(r"[^A-Za-z0-9._-]+", "_", public_url)
609
+ runtime_root = str(os.getenv("KAPO_RUNTIME_ROOT") or _sync_target_root() or "brain_runtime").strip()
610
+ return re.sub(r"[^A-Za-z0-9._-]+", "_", runtime_root)
611
+
612
+
613
+ def _applied_runtime_version_path() -> Path:
614
+ runtime_root = Path(_sync_target_root()).resolve()
615
+ state_dir = runtime_root / "data" / "local" / "brain_runtime"
616
+ state_dir.mkdir(parents=True, exist_ok=True)
617
+ return state_dir / "applied_version.json"
618
+
619
+
620
+ def _load_applied_runtime_version() -> dict[str, Any]:
621
+ try:
622
+ return dict(json.loads(_applied_runtime_version_path().read_text(encoding="utf-8")) or {})
623
+ except Exception:
624
+ return {}
625
+
626
+
627
+ def _write_applied_runtime_version(payload: dict[str, Any]) -> None:
628
+ _applied_runtime_version_path().write_text(
629
+ json.dumps(dict(payload or {}), ensure_ascii=False, indent=2, sort_keys=True),
630
+ encoding="utf-8",
631
+ )
632
+
633
+
634
+ def _download_remote_zip(url: str, destination: Path) -> Path:
635
+ response = requests.get(str(url).strip(), timeout=120)
636
+ response.raise_for_status()
637
+ destination.parent.mkdir(parents=True, exist_ok=True)
638
+ destination.write_bytes(response.content)
639
+ return destination
640
+
641
+
642
+ def _apply_zip_overlay(zip_path: Path, target_root: Path) -> None:
643
+ with zipfile.ZipFile(zip_path, "r") as archive:
644
+ archive.extractall(target_root)
645
+
646
+
647
+ def _load_patch_manifest_from_bootstrap() -> dict[str, Any]:
648
+ bootstrap = DRIVE_STATE.ensure_bootstrap_loaded(force=True) or {}
649
+ manifest_url = str(bootstrap.get("patch_manifest_url") or "").strip()
650
+ if not manifest_url:
651
+ return dict(bootstrap)
652
+ try:
653
+ response = requests.get(manifest_url, timeout=30)
654
+ response.raise_for_status()
655
+ payload = dict(response.json() or {})
656
+ merged = {**bootstrap, **payload}
657
+ return merged
658
+ except Exception:
659
+ logger.warning("Failed to load patch manifest from bootstrap URL", exc_info=True)
660
+ return dict(bootstrap)
661
+
662
+
663
+ def _run_startup_self_update() -> None:
664
+ if not _startup_self_update_enabled():
665
+ return
666
+ manifest = _load_patch_manifest_from_bootstrap()
667
+ target_version = str(manifest.get("version") or "").strip()
668
+ target_hash = str(manifest.get("build_hash") or "").strip()
669
+ if not target_version and not target_hash:
670
+ return
671
+ current = _load_applied_runtime_version()
672
+ if (
673
+ str(current.get("version") or "").strip() == target_version
674
+ and str(current.get("build_hash") or "").strip() == target_hash
675
+ and target_version
676
+ ):
677
+ return
678
+ runtime_root = Path(_sync_target_root()).resolve()
679
+ temp_dir = runtime_root / "data" / "local" / "brain_runtime" / "updates"
680
+ patch_url = str(manifest.get("patch_bundle_url") or "").strip()
681
+ full_url = str(manifest.get("full_package_url") or "").strip()
682
+ applied_mode = ""
683
+ source_url = ""
684
+ try:
685
+ if patch_url:
686
+ zip_path = _download_remote_zip(patch_url, temp_dir / "patch_bundle.zip")
687
+ _apply_zip_overlay(zip_path, runtime_root)
688
+ applied_mode = "patch_bundle"
689
+ source_url = patch_url
690
+ elif full_url:
691
+ zip_path = _download_remote_zip(full_url, temp_dir / "full_package.zip")
692
+ _apply_zip_overlay(zip_path, runtime_root)
693
+ applied_mode = "full_package"
694
+ source_url = full_url
695
+ else:
696
+ return
697
+ _write_applied_runtime_version(
698
+ {
699
+ "version": target_version,
700
+ "build_hash": target_hash,
701
+ "applied_at": time.time(),
702
+ "mode": applied_mode,
703
+ "source_url": source_url,
704
+ }
705
+ )
706
+ logger.info("Applied startup self-update (%s) version=%s", applied_mode, target_version or target_hash)
707
+ except Exception:
708
+ logger.warning("Startup self-update failed", exc_info=True)
709
+
710
+
711
+ def _remember_ngrok_api_url(api_url: str) -> None:
712
+ value = str(api_url or "").strip().rstrip("/")
713
+ if not value:
714
+ return
715
+ os.environ["KAPO_NGROK_API_URL"] = value
716
+ try:
717
+ _ngrok_api_state_path().write_text(value, encoding="utf-8")
718
+ except Exception:
719
+ logger.warning("Failed to persist ngrok API URL", exc_info=True)
720
+
721
+
722
+ def _load_saved_ngrok_api_url() -> str:
723
+ configured = str(os.getenv("KAPO_NGROK_API_URL", "")).strip().rstrip("/")
724
+ if configured:
725
+ return configured
726
+ try:
727
+ return _ngrok_api_state_path().read_text(encoding="utf-8").strip().rstrip("/")
728
+ except Exception:
729
+ return ""
730
+
731
+
732
+ def _ngrok_api_candidates() -> list[str]:
733
+ seen: set[str] = set()
734
+ candidates: list[str] = []
735
+ 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"]:
736
+ value = str(candidate or "").strip().rstrip("/")
737
+ if value and value not in seen:
738
+ seen.add(value)
739
+ candidates.append(value)
740
+ return candidates
741
+
742
+
743
+ def _probe_ngrok_api(api_url: str) -> bool:
744
+ try:
745
+ response = requests.get(f"{api_url}/api/tunnels", timeout=2)
746
+ return response.status_code == 200
747
+ except Exception:
748
+ return False
749
+
750
+
751
+ def _find_live_ngrok_api() -> str | None:
752
+ for api_url in _ngrok_api_candidates():
753
+ if _probe_ngrok_api(api_url):
754
+ _remember_ngrok_api_url(api_url)
755
+ return api_url
756
+ return None
757
+
758
+
759
+ def _ngrok_binary_path() -> str:
760
+ env_path = str(os.getenv("NGROK_PATH", "")).strip()
761
+ if env_path and Path(env_path).exists():
762
+ return env_path
763
+ default_ngrok_path = ""
764
+ try:
765
+ from pyngrok import conf
766
+
767
+ default_ngrok_path = str(conf.get_default().ngrok_path or "").strip()
768
+ if default_ngrok_path and Path(default_ngrok_path).exists():
769
+ return default_ngrok_path
770
+ except Exception:
771
+ pass
772
+ try:
773
+ from pyngrok import installer
774
+
775
+ install_target = default_ngrok_path or str((Path.home() / ".ngrok" / "ngrok").resolve())
776
+ Path(install_target).parent.mkdir(parents=True, exist_ok=True)
777
+ installer.install_ngrok(install_target)
778
+ if Path(install_target).exists():
779
+ return install_target
780
+ except Exception:
781
+ logger.warning("Failed to auto-install ngrok binary", exc_info=True)
782
+ discovered = shutil.which("ngrok")
783
+ if discovered:
784
+ return discovered
785
+ return "ngrok"
786
+
787
+
788
+ def _ensure_ngrok_auth(token: str) -> None:
789
+ ngrok_path = _ngrok_binary_path()
790
+ subprocess.run(
791
+ [ngrok_path, "config", "add-authtoken", token],
792
+ check=False,
793
+ stdout=subprocess.DEVNULL,
794
+ stderr=subprocess.DEVNULL,
795
+ )
796
+
797
+
798
+ def _start_detached_ngrok_agent(token: str) -> str | None:
799
+ if token:
800
+ _ensure_ngrok_auth(token)
801
+ os.environ["NGROK_AUTHTOKEN"] = token
802
+
803
+ ngrok_path = _ngrok_binary_path()
804
+ popen_kwargs = {
805
+ "stdout": subprocess.DEVNULL,
806
+ "stderr": subprocess.DEVNULL,
807
+ "stdin": subprocess.DEVNULL,
808
+ }
809
+ if os.name == "nt":
810
+ popen_kwargs["creationflags"] = getattr(subprocess, "DETACHED_PROCESS", 0) | getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
811
+ else:
812
+ popen_kwargs["start_new_session"] = True
813
+
814
+ subprocess.Popen(
815
+ [ngrok_path, "start", "--none", "--log=stdout"],
816
+ **popen_kwargs,
817
+ )
818
+
819
+ deadline = time.time() + 12
820
+ while time.time() < deadline:
821
+ api_url = _find_live_ngrok_api()
822
+ if api_url:
823
+ return api_url
824
+ time.sleep(0.5)
825
+ return None
826
+
827
+
828
+ def _list_ngrok_tunnels(api_url: str) -> list[dict[str, Any]]:
829
+ response = requests.get(f"{api_url}/api/tunnels", timeout=5)
830
+ response.raise_for_status()
831
+ payload = response.json()
832
+ tunnels = payload.get("tunnels")
833
+ return tunnels if isinstance(tunnels, list) else []
834
+
835
+
836
+ def _existing_ngrok_public_url(api_url: str, port: int) -> str | None:
837
+ for tunnel in _list_ngrok_tunnels(api_url):
838
+ public_url = str(tunnel.get("public_url") or "").strip()
839
+ config = tunnel.get("config") or {}
840
+ addr = str(config.get("addr") or "").strip()
841
+ if public_url and addr.endswith(f":{port}"):
842
+ return public_url.rstrip("/")
843
+ return None
844
+
845
+
846
+ def _create_ngrok_tunnel(api_url: str, port: int) -> str | None:
847
+ response = requests.post(
848
+ f"{api_url}/api/tunnels",
849
+ json={
850
+ "name": f"http-{port}-kapo",
851
+ "addr": str(port),
852
+ "proto": "http",
853
+ },
854
+ timeout=10,
855
+ )
856
+ response.raise_for_status()
857
+ payload = response.json()
858
+ return str(payload.get("public_url") or "").strip().rstrip("/") or None
859
+
860
+
861
+ def _publish_brain_presence(public_url: str, *, source: str = "runtime") -> None:
862
+ normalized = str(public_url or "").strip().rstrip("/")
863
+ if not normalized:
864
+ return
865
+ payload = {
866
+ "url": normalized,
867
+ "status": "healthy",
868
+ "source": source,
869
+ "platform": "kaggle" if _is_kaggle_runtime() else str(os.getenv("BRAIN_PROVIDER") or "remote"),
870
+ "role": os.getenv("BRAIN_PRIMARY_ROLE", "fallback"),
871
+ "roles": [part.strip() for part in os.getenv("BRAIN_ROLES", "supervisor,chat,coding,planner,arabic,fallback").split(",") if part.strip()],
872
+ "languages": [part.strip() for part in os.getenv("BRAIN_LANGUAGES", "ar,en").split(",") if part.strip()],
873
+ "model_profile_id": os.getenv("MODEL_PROFILE_ID") or os.getenv("SUPERVISOR_MODEL_PROFILE_ID") or DEFAULT_MODEL_PROFILE_ID,
874
+ "model_repo": MODEL_META.get("repo_id") or os.getenv("MODEL_REPO") or DEFAULT_MODEL_REPO,
875
+ "model_file": MODEL_META.get("filename") or os.getenv("MODEL_FILE") or DEFAULT_MODEL_FILE,
876
+ "updated_at": time.time(),
877
+ }
878
+ FIREBASE.set_document("brains", normalized, payload)
879
+ FIREBASE.set_document(
880
+ "runtime",
881
+ "brains_last_report",
882
+ {
883
+ "brain_url": normalized,
884
+ "source": source,
885
+ "updated_at": time.time(),
886
+ "provider": payload["platform"],
887
+ "model_profile_id": payload["model_profile_id"],
888
+ },
889
+ )
890
+ FIREBASE.set_document(
891
+ "tunnels",
892
+ f"brain_{normalized}",
893
+ {
894
+ "kind": "brain",
895
+ "public_url": normalized,
896
+ "provider": "ngrok" if "ngrok" in normalized else payload["platform"],
897
+ "updated_at": time.time(),
898
+ },
899
+ )
900
+
901
+
902
+ def _report_brain_url(public_url: str) -> None:
903
+ _publish_brain_presence(public_url, source="report_attempt")
904
+ executor_url = os.getenv("EXECUTOR_URL", "").strip().rstrip("/")
905
+ if not executor_url:
906
+ return
907
+ if not _should_report_brain_url(public_url):
908
+ return
909
+ last_error: Exception | None = None
910
+ connect_timeout = max(1.0, float(os.getenv("BRAIN_REPORT_CONNECT_TIMEOUT_SEC", "4.0") or 4.0))
911
+ read_timeout = max(5.0, float(os.getenv("BRAIN_REPORT_READ_TIMEOUT_SEC", "15.0") or 15.0))
912
+ retries = max(1, int(os.getenv("BRAIN_REPORT_RETRIES", "2") or 2))
913
+ for _ in range(retries):
914
+ try:
915
+ response = requests.post(
916
+ f"{executor_url}/brain/report-url",
917
+ json={
918
+ "brain_url": public_url,
919
+ "platform": "kaggle" if _is_kaggle_runtime() else "remote",
920
+ "role": os.getenv("BRAIN_PRIMARY_ROLE", "fallback"),
921
+ "roles": [part.strip() for part in os.getenv("BRAIN_ROLES", "supervisor,chat,coding,planner,arabic,fallback").split(",") if part.strip()],
922
+ "languages": [part.strip() for part in os.getenv("BRAIN_LANGUAGES", "ar,en").split(",") if part.strip()],
923
+ "model_profile_id": os.getenv("MODEL_PROFILE_ID") or os.getenv("SUPERVISOR_MODEL_PROFILE_ID") or DEFAULT_MODEL_PROFILE_ID,
924
+ "model_repo": MODEL_META.get("repo_id") or os.getenv("MODEL_REPO") or DEFAULT_MODEL_REPO,
925
+ "model_file": MODEL_META.get("filename") or os.getenv("MODEL_FILE") or DEFAULT_MODEL_FILE,
926
+ },
927
+ headers=_brain_headers(),
928
+ timeout=(connect_timeout, read_timeout),
929
+ )
930
+ response.raise_for_status()
931
+ _publish_brain_presence(public_url, source="executor_report")
932
+ return
933
+ except Exception as exc:
934
+ last_error = exc
935
+ time.sleep(1)
936
+ logger.info(
937
+ "Brain URL report to executor timed out or failed; continuing (%s)",
938
+ last_error,
939
+ )
940
+
941
+
942
+ def _pull_executor_settings() -> dict[str, Any]:
943
+ if _shared_state_backend() in {"google_drive", "drive", "gdrive"} or _drive_bootstrap_configured():
944
+ return {}
945
+ executor_url = _load_saved_executor_url()
946
+ if not executor_url:
947
+ return {}
948
+ try:
949
+ response = requests.get(
950
+ f"{executor_url}/share/settings",
951
+ headers=_brain_headers(),
952
+ timeout=(
953
+ max(1.5, float(os.getenv("EXECUTOR_CONNECT_TIMEOUT_SEC", "3.0") or 3.0)),
954
+ max(2.0, float(os.getenv("EXECUTOR_SETTINGS_READ_TIMEOUT_SEC", "5.0") or 5.0)),
955
+ ),
956
+ )
957
+ if response.status_code == 200:
958
+ return response.json()
959
+ logger.warning("Executor settings request failed: %s", response.text[:400])
960
+ except Exception as exc:
961
+ logger.warning("Failed to pull executor settings (%s)", exc)
962
+ return {}
963
+
964
+
965
+ def start_ngrok(token: str | None = None) -> str | None:
966
+ restart_reuse = str(os.getenv("KAPO_RESTART_REUSE_PUBLIC_URL", "")).strip().lower() in {"1", "true", "yes", "on"}
967
+ if restart_reuse:
968
+ saved_public_url = _load_saved_public_url()
969
+ if saved_public_url:
970
+ _remember_public_url(saved_public_url)
971
+ _report_brain_url(saved_public_url)
972
+ FIREBASE.set_document("brains", saved_public_url, {"url": saved_public_url, "status": "healthy", "source": "restart_reuse"})
973
+ FIREBASE.set_document("tunnels", f"brain_{saved_public_url}", {"kind": "brain", "public_url": saved_public_url, "provider": "ngrok"})
974
+ logger.info("Reusing saved brain public URL after restart: %s", saved_public_url)
975
+ os.environ["KAPO_RESTART_REUSE_PUBLIC_URL"] = "0"
976
+ return saved_public_url
977
+
978
+ configured_public_url = _configured_public_url()
979
+ if configured_public_url and _prefer_configured_public_url():
980
+ _remember_public_url(configured_public_url)
981
+ _report_brain_url(configured_public_url)
982
+ FIREBASE.set_document("brains", configured_public_url, {"url": configured_public_url, "status": "healthy", "source": "configured_public_url"})
983
+ FIREBASE.set_document("tunnels", f"brain_{configured_public_url}", {"kind": "brain", "public_url": configured_public_url, "provider": "configured"})
984
+ logger.info("Using configured brain public URL without starting ngrok: %s", configured_public_url)
985
+ return configured_public_url
986
+
987
+ if not _ngrok_bootstrap_enabled():
988
+ logger.info("Skipping ngrok bootstrap because BRAIN_AUTO_NGROK is disabled")
989
+ return None
990
+
991
+ try:
992
+ authtoken = str(token or os.getenv("NGROK_AUTHTOKEN") or "").strip()
993
+ if not authtoken:
994
+ return None
995
+
996
+ port = int(os.getenv("BRAIN_PORT", "7860"))
997
+ api_url = _find_live_ngrok_api()
998
+ if not api_url:
999
+ api_url = _start_detached_ngrok_agent(authtoken)
1000
+ if not api_url:
1001
+ logger.warning("Ngrok agent did not expose a local API URL")
1002
+ return None
1003
+
1004
+ public_url = _existing_ngrok_public_url(api_url, port)
1005
+ if not public_url:
1006
+ public_url = _create_ngrok_tunnel(api_url, port)
1007
+ if not public_url:
1008
+ return None
1009
+
1010
+ match = re.search(r"https://[A-Za-z0-9.-]+", public_url)
1011
+ if match:
1012
+ public_url = match.group(0)
1013
+ _remember_ngrok_api_url(api_url)
1014
+ _remember_public_url(public_url)
1015
+ _report_brain_url(public_url)
1016
+ FIREBASE.set_document("brains", public_url, {"url": public_url, "status": "healthy", "source": "ngrok_bootstrap"})
1017
+ FIREBASE.set_document("tunnels", f"brain_{public_url}", {"kind": "brain", "public_url": public_url, "provider": "ngrok", "api_url": api_url})
1018
+ return public_url
1019
+ except Exception:
1020
+ logger.exception("Ngrok startup failed")
1021
+ return None
1022
+
1023
+
1024
+ def _report_known_public_url() -> str | None:
1025
+ public_url = _load_saved_public_url()
1026
+ if not public_url:
1027
+ return None
1028
+ _remember_public_url(public_url)
1029
+ _report_brain_url(public_url)
1030
+ FIREBASE.set_document("brains", public_url, {"url": public_url, "status": "healthy", "source": "saved_public_url"})
1031
+ logger.info("Reported known brain public URL without starting ngrok: %s", public_url)
1032
+ return public_url
1033
+
1034
+
1035
+ def _bootstrap_executor_handshake(start_tunnel: bool = False) -> None:
1036
+ executor_url = os.getenv("EXECUTOR_URL", "").strip()
1037
+ if not executor_url:
1038
+ if start_tunnel:
1039
+ public_url = start_ngrok(os.getenv("NGROK_AUTHTOKEN") or None)
1040
+ if public_url:
1041
+ logger.info("Brain public URL started locally without executor handshake: %s", public_url)
1042
+ else:
1043
+ logger.info("Brain started without publishing a public URL")
1044
+ return
1045
+ logger.info("Skipping executor handshake: EXECUTOR_URL not configured")
1046
+ return
1047
+
1048
+ settings = _pull_executor_settings()
1049
+ _apply_executor_settings(settings)
1050
+
1051
+ public_url = None
1052
+ if start_tunnel:
1053
+ public_url = start_ngrok(os.getenv("NGROK_AUTHTOKEN") or None)
1054
+ if not public_url:
1055
+ public_url = _report_known_public_url()
1056
+ else:
1057
+ public_url = _report_known_public_url()
1058
+ if public_url:
1059
+ logger.info("Brain public URL reported to executor: %s", public_url)
1060
+ else:
1061
+ logger.info("Brain started without publishing a public URL")
1062
+
1063
+
1064
+ @app.on_event("startup")
1065
+ async def startup_event():
1066
+ global RUNTIME_STATE_THREAD_STARTED
1067
+ try:
1068
+ _bootstrap_shared_state()
1069
+ except Exception:
1070
+ logger.exception("Shared-state bootstrap failed")
1071
+ try:
1072
+ _prepare_runtime_environment()
1073
+ except Exception:
1074
+ logger.exception("Runtime environment bootstrap failed")
1075
+ try:
1076
+ _run_startup_self_update()
1077
+ except Exception:
1078
+ logger.exception("Startup self-update bootstrap failed")
1079
+ internal_restart = _internal_restart_in_progress()
1080
+ fast_restart = internal_restart and _fast_restart_enabled()
1081
+ if not fast_restart:
1082
+ try:
1083
+ settings = _pull_executor_settings()
1084
+ _apply_executor_settings(settings)
1085
+ except Exception:
1086
+ logger.exception("Executor settings bootstrap failed")
1087
+ try:
1088
+ _apply_firebase_runtime_settings()
1089
+ except Exception:
1090
+ logger.exception("Firebase runtime bootstrap failed")
1091
+ else:
1092
+ logger.info("Fast internal restart enabled; skipping executor/Firebase startup bootstrap")
1093
+ if not fast_restart:
1094
+ _load_default_model()
1095
+ try:
1096
+ if not fast_restart:
1097
+ _load_embed_model()
1098
+ except Exception:
1099
+ logger.exception("Embedding model startup failed")
1100
+ try:
1101
+ start_tunnel = _auto_publish_public_url_on_startup() and not internal_restart
1102
+ _bootstrap_executor_handshake(start_tunnel=start_tunnel)
1103
+ except Exception:
1104
+ logger.exception("Executor handshake startup failed")
1105
+ finally:
1106
+ os.environ["KAPO_INTERNAL_RESTART"] = "0"
1107
+ _persist_runtime_state_snapshot(reason="startup")
1108
+ if not RUNTIME_STATE_THREAD_STARTED:
1109
+ RUNTIME_STATE_THREAD_STARTED = True
1110
+ threading.Thread(target=_runtime_state_pulse, daemon=True).start()
1111
+
1112
+
1113
+ class ModelLoadRequest(BaseModel):
1114
+ repo_id: str
1115
+ filename: str
1116
+ hf_token: str | None = None
1117
+
1118
+
1119
+ class ConnectionInit(BaseModel):
1120
+ executor_url: str
1121
+ ngrok_token: str | None = None
1122
+
1123
+
1124
+ class PublishUrlRequest(BaseModel):
1125
+ ngrok_token: str | None = None
1126
+ public_url: str | None = None
1127
+ start_tunnel: bool = True
1128
+
1129
+
1130
+ class RestartRequest(BaseModel):
1131
+ delay_sec: float = 1.0
1132
+
1133
+
1134
+ class FileWriteRequest(BaseModel):
1135
+ path: str
1136
+ content: str = ""
1137
+ overwrite: bool = True
1138
+
1139
+
1140
+ class FileDeleteRequest(BaseModel):
1141
+ path: str
1142
+ recursive: bool = False
1143
+
1144
+
1145
+ class FileMkdirRequest(BaseModel):
1146
+ path: str
1147
+
1148
+
1149
+ class ChatRequest(BaseModel):
1150
+ request_id: str
1151
+ user_input: str
1152
+ context: dict[str, Any] = {}
1153
+ history: list[dict[str, str]] = []
1154
+ auto_execute: bool = True
1155
+
1156
+
1157
+ def _contains_arabic(text: str) -> bool:
1158
+ return bool(re.search(r"[\u0600-\u06FF]", text or ""))
1159
+
1160
+
1161
+ def _detect_language(text: str) -> str:
1162
+ return "ar" if _contains_arabic(text) else "en"
1163
+
1164
+
1165
+ def _is_task_request(text: str) -> bool:
1166
+ lower = (text or "").strip().lower()
1167
+ task_words = [
1168
+ "build", "fix", "debug", "create project", "generate project", "implement", "refactor",
1169
+ "run", "execute", "install", "modify", "edit", "update", "write code", "make app",
1170
+ "انشئ", "أنشئ", "اعمل", "نفذ", "شغل", "اصلح", "أصلح", "عدّل", "عدل", "ابني", "كوّن مشروع",
1171
+ ]
1172
+ return any(word in lower for word in task_words)
1173
+
1174
+
1175
+ def _is_research_request(text: str) -> bool:
1176
+ lower = (text or "").strip().lower()
1177
+ research_words = [
1178
+ "search", "research", "look up", "find out", "web", "browse",
1179
+ "ابحث", "ابحث عن", "دور", "فتش", "معلومة عن", "معلومات عن",
1180
+ ]
1181
+ return any(word in lower for word in research_words)
1182
+
1183
+
1184
+ def _is_knowledge_request(text: str, context: dict[str, Any] | None = None) -> bool:
1185
+ context = context or {}
1186
+ if bool(context.get("use_executor_knowledge")):
1187
+ return True
1188
+ lower = (text or "").strip().lower()
1189
+ knowledge_words = [
1190
+ "remember", "memory", "knowledge", "docs", "documentation", "project structure", "architecture",
1191
+ "تذكر", "الذاكرة", "المعرفة", "الوثائق", "الدليل", "بنية المشروع", "هيكل المشروع", "معمارية",
1192
+ ]
1193
+ return any(word in lower for word in knowledge_words)
1194
+
1195
+
1196
+ def _prune_history(history: list[dict[str, str]], keep_last: int = 6) -> list[dict[str, str]]:
1197
+ if len(history) <= keep_last:
1198
+ return history
1199
+ return history[-keep_last:]
1200
+
1201
+
1202
+ def _retrieve_knowledge(query: str, top_k: int = 4) -> list[dict[str, Any]]:
1203
+ executor_url = os.getenv("EXECUTOR_URL", "").strip().rstrip("/")
1204
+ expanded_query = _expand_project_query(query)
1205
+ if _executor_roundtrip_allowed("BRAIN_REMOTE_KNOWLEDGE_ENABLED", default=True):
1206
+ try:
1207
+ response = requests.get(
1208
+ f"{executor_url}/rag/search",
1209
+ params={"query": expanded_query, "top_k": top_k},
1210
+ headers=_brain_headers(),
1211
+ timeout=(
1212
+ _executor_connect_timeout(),
1213
+ _executor_read_timeout("BRAIN_REMOTE_KNOWLEDGE_TIMEOUT_SEC", 6.0),
1214
+ ),
1215
+ )
1216
+ if response.status_code == 200:
1217
+ payload = response.json()
1218
+ results = payload.get("results", [])
1219
+ if isinstance(results, list):
1220
+ return results
1221
+ except requests.exceptions.ReadTimeout:
1222
+ logger.info("Executor knowledge retrieval timed out; continuing without remote knowledge")
1223
+ except Exception:
1224
+ logger.warning("Executor knowledge retrieval failed", exc_info=True)
1225
+ if _remote_brain_only() or not _feature_enabled("BRAIN_LOCAL_RAG_FALLBACK_ENABLED", default=False):
1226
+ return []
1227
+ try:
1228
+ from rag.retriever import retrieve
1229
+
1230
+ return retrieve(expanded_query, top_k=top_k)
1231
+ except Exception:
1232
+ logger.warning("Knowledge retrieval failed", exc_info=True)
1233
+ return []
1234
+
1235
+
1236
+ def _search_web(query: str) -> list[dict[str, Any]]:
1237
+ executor_url = os.getenv("EXECUTOR_URL", "").strip().rstrip("/")
1238
+ if not _executor_roundtrip_allowed("BRAIN_REMOTE_WEB_SEARCH_ENABLED", default=True):
1239
+ return []
1240
+ try:
1241
+ response = requests.post(
1242
+ f"{executor_url}/tools/search",
1243
+ json={"query": query, "num_results": 5},
1244
+ headers=_brain_headers(),
1245
+ timeout=(
1246
+ _executor_connect_timeout(),
1247
+ _executor_read_timeout("BRAIN_REMOTE_WEB_SEARCH_TIMEOUT_SEC", 8.0),
1248
+ ),
1249
+ )
1250
+ if response.status_code == 200:
1251
+ payload = response.json()
1252
+ results = payload.get("results", [])
1253
+ return results if isinstance(results, list) else []
1254
+ except Exception:
1255
+ logger.warning("Web search failed", exc_info=True)
1256
+ return []
1257
+
1258
+
1259
+ def _format_context_blocks(knowledge: list[dict[str, Any]], web_results: list[dict[str, Any]]) -> str:
1260
+ blocks: list[str] = []
1261
+ if knowledge:
1262
+ lines = []
1263
+ for item in knowledge[:4]:
1264
+ source = item.get("source", "knowledge")
1265
+ content = item.get("content", "") or item.get("text", "")
1266
+ lines.append(f"- [{source}] {content[:500]}")
1267
+ blocks.append("Knowledge:\n" + "\n".join(lines))
1268
+ if web_results:
1269
+ lines = []
1270
+ for item in web_results[:5]:
1271
+ lines.append(f"- {item.get('title', '')}: {item.get('snippet', '')}")
1272
+ blocks.append("Web:\n" + "\n".join(lines))
1273
+ return "\n\n".join(blocks).strip()
1274
+
1275
+
1276
+ def _project_context_tags(text: str) -> list[str]:
1277
+ source = str(text or "")
1278
+ lowered = source.lower()
1279
+ tags: list[str] = []
1280
+ tag_rules = [
1281
+ ("brain_runtime", ["العقل", "brain", "model", "موديل", "النموذج"]),
1282
+ ("executor_runtime", ["الوكيل التنفيذي", "executor", "agent"]),
1283
+ ("tunnel_runtime", ["النفق", "tunnel", "ngrok", "cloudflare"]),
1284
+ ("url_routing", ["الرابط", "url", "endpoint", "لينك"]),
1285
+ ("restart_sync", ["restart", "ريستارت", "إعادة تشغيل", "اعادة تشغيل", "sync", "مزامنة"]),
1286
+ ("knowledge_memory", ["memory", "ذاكرة", "معرفة", "knowledge", "rag", "طبقات", "embedding", "embeddings"]),
1287
+ ("kaggle_runtime", ["kaggle", "كاجل"]),
1288
+ ]
1289
+ for tag, markers in tag_rules:
1290
+ if any(marker in source or marker in lowered for marker in markers):
1291
+ tags.append(tag)
1292
+ return tags
1293
+
1294
+
1295
+ def _expand_project_query(query: str) -> str:
1296
+ tags = _project_context_tags(query)
1297
+ additions: list[str] = []
1298
+ if "tunnel_runtime" in tags:
1299
+ additions.append("ngrok tunnel public url reverse proxy runtime restart")
1300
+ if "restart_sync" in tags:
1301
+ additions.append("system restart sync uvicorn process reuse public url")
1302
+ if "executor_runtime" in tags:
1303
+ additions.append("executor share settings control plane local machine")
1304
+ if "knowledge_memory" in tags:
1305
+ additions.append("knowledge layers rag embeddings preferences profile")
1306
+ if "brain_runtime" in tags:
1307
+ additions.append("brain server kaggle model startup runtime")
1308
+ return query if not additions else f"{query}\nContext expansion: {' | '.join(additions)}"
1309
+
1310
+
1311
+ def _fetch_style_profile() -> dict[str, Any]:
1312
+ firebase_profile = FIREBASE.get_document("profiles", "style", ttl_sec=30.0)
1313
+ if firebase_profile:
1314
+ return firebase_profile
1315
+ executor_url = os.getenv("EXECUTOR_URL", "").strip().rstrip("/")
1316
+ if not executor_url or not _executor_roundtrip_allowed("BRAIN_REMOTE_STYLE_PROFILE_ENABLED", default=False):
1317
+ return {}
1318
+ try:
1319
+ response = requests.get(
1320
+ f"{executor_url}/preferences/profile",
1321
+ headers=_brain_headers(),
1322
+ timeout=(
1323
+ _executor_connect_timeout(),
1324
+ _executor_read_timeout("BRAIN_REMOTE_STYLE_PROFILE_TIMEOUT_SEC", 2.5),
1325
+ ),
1326
+ )
1327
+ if response.status_code != 200:
1328
+ return {}
1329
+ payload = response.json().get("profile", {})
1330
+ return payload if isinstance(payload, dict) else {}
1331
+ except requests.exceptions.ReadTimeout:
1332
+ logger.info("Style profile load timed out; continuing without remote style profile")
1333
+ return {}
1334
+ except Exception:
1335
+ logger.warning("Failed to load style profile", exc_info=True)
1336
+ return {}
1337
+
1338
+
1339
+ def _render_style_profile_context(profile: dict[str, Any]) -> str:
1340
+ if not profile:
1341
+ return ""
1342
+ preferences = profile.get("preferences", []) or []
1343
+ examples = profile.get("examples", []) or []
1344
+ lexical_signals = profile.get("lexical_signals", []) or []
1345
+ style_markers = profile.get("style_markers", {}) or {}
1346
+ persona_summary = str(profile.get("persona_summary") or "").strip()
1347
+ response_contract = str(profile.get("response_contract") or "").strip()
1348
+ lines: list[str] = []
1349
+ if persona_summary:
1350
+ lines.append(f"User persona summary: {persona_summary}")
1351
+ if response_contract:
1352
+ lines.append(f"Response contract: {response_contract}")
1353
+ if preferences:
1354
+ lines.append("User Style Preferences:")
1355
+ for item in preferences[:10]:
1356
+ lines.append(f"- {item}")
1357
+ if lexical_signals:
1358
+ lines.append("User Lexical Signals:")
1359
+ for item in lexical_signals[:10]:
1360
+ lines.append(f"- {item}")
1361
+ if style_markers:
1362
+ lines.append("Style Markers:")
1363
+ for key, value in sorted(style_markers.items()):
1364
+ lines.append(f"- {key}: {value}")
1365
+ if examples:
1366
+ lines.append("Recent User Style Examples:")
1367
+ for sample in examples[-3:]:
1368
+ user_text = str(sample.get("user_input") or "").strip()
1369
+ assistant_text = str(sample.get("assistant_reply") or "").strip()
1370
+ if user_text:
1371
+ lines.append(f"- User: {user_text}")
1372
+ if assistant_text:
1373
+ lines.append(f" Assistant: {assistant_text}")
1374
+ return "\n".join(lines).strip()
1375
+
1376
+
1377
+ def _project_domain_context(user_input: str, context: dict[str, Any] | None = None) -> str:
1378
+ tags = _project_context_tags(user_input)
1379
+ if not tags:
1380
+ return ""
1381
+ lines = [
1382
+ "Project Domain Glossary:",
1383
+ "- In this project, terms like العقل, الوكيل التنفيذي, النفق, الرابط, الريستارت, المزامنة, الذاكرة, والطبقات usually refer to software runtime and operations concepts.",
1384
+ "- Treat النفق as ngrok or cloudflare tunnel unless the user explicitly asks for a literal/civil meaning.",
1385
+ "- Treat الرابط as public URL, endpoint, or routing target when the surrounding context mentions deployment, Kaggle, restart, or sync.",
1386
+ "- Treat العقل as the remote Brain service/model runtime, and الوكيل التنفيذي as the local executor/control plane on the user's device.",
1387
+ ]
1388
+ if "restart_sync" in tags:
1389
+ lines.append("- Restart means process/service restart; preserving the same public URL matters more than creating a fresh tunnel.")
1390
+ if "knowledge_memory" in tags:
1391
+ lines.append("- Knowledge, embeddings, layers, and memory refer to the RAG and memory system inside this project.")
1392
+ if "kaggle_runtime" in tags:
1393
+ lines.append("- Kaggle here is the remote runtime hosting the Brain service.")
1394
+ role_name = str((context or {}).get("role_name") or "").strip()
1395
+ if role_name:
1396
+ lines.append(f"- Current assigned role: {role_name}.")
1397
+ return "\n".join(lines)
1398
+
1399
+
1400
+ def _firebase_runtime_context(role_name: str, language: str) -> str:
1401
+ snapshot = _firebase_runtime_snapshot()
1402
+ lines: list[str] = []
1403
+ roles = snapshot.get("roles") or []
1404
+ if roles:
1405
+ enabled_roles = [
1406
+ str(item.get("name") or item.get("id") or "").strip()
1407
+ for item in roles
1408
+ if str(item.get("enabled", True)).strip().lower() not in {"0", "false", "no", "off"}
1409
+ ]
1410
+ enabled_roles = [item for item in enabled_roles if item]
1411
+ if enabled_roles:
1412
+ lines.append("Live roles from Firestore: " + ", ".join(enabled_roles[:12]))
1413
+ models = snapshot.get("models") or []
1414
+ if models:
1415
+ preferred = [
1416
+ item for item in models
1417
+ if str(item.get("enabled", True)).strip().lower() not in {"0", "false", "no", "off"}
1418
+ ]
1419
+ if preferred:
1420
+ labels = [str(item.get("label") or item.get("id") or "").strip() for item in preferred[:5]]
1421
+ labels = [item for item in labels if item]
1422
+ if labels:
1423
+ lines.append("Live model profiles: " + ", ".join(labels))
1424
+ platforms = snapshot.get("platforms") or []
1425
+ if platforms:
1426
+ names = [str(item.get("name") or "").strip() for item in platforms[:6] if str(item.get("name") or "").strip()]
1427
+ if names:
1428
+ lines.append("Live platforms: " + ", ".join(names))
1429
+ prompt_body = _firebase_prompt_body(role_name or "chat", language) or _firebase_prompt_body(role_name or "chat", "en")
1430
+ if prompt_body:
1431
+ lines.append(f"Live Firestore prompt for role '{role_name or 'chat'}': {prompt_body}")
1432
+ return "\n".join(lines).strip()
1433
+
1434
+
1435
+ def _append_runtime_instructions(context_block: str, context: dict[str, Any]) -> str:
1436
+ instructions = str((context or {}).get("system_instructions") or "").strip()
1437
+ role_name = str((context or {}).get("role_name") or "").strip()
1438
+ user_input = str((context or {}).get("user_input") or "").strip()
1439
+ language = _detect_language(user_input)
1440
+ style_profile = _render_style_profile_context(_fetch_style_profile())
1441
+ domain_context = _project_domain_context(user_input, context)
1442
+ firebase_context = _firebase_runtime_context(role_name, language)
1443
+ if not instructions and not role_name and not style_profile and not domain_context and not firebase_context:
1444
+ return context_block
1445
+ extra = []
1446
+ if role_name:
1447
+ extra.append(f"Assigned role: {role_name}")
1448
+ if instructions:
1449
+ extra.append(instructions)
1450
+ if firebase_context:
1451
+ extra.append(firebase_context)
1452
+ if style_profile:
1453
+ extra.append(style_profile)
1454
+ if domain_context:
1455
+ extra.append(domain_context)
1456
+ extra_block = "Runtime Instructions:\n" + "\n".join(extra)
1457
+ return (context_block + "\n\n" + extra_block).strip() if context_block else extra_block
1458
+
1459
+
1460
+ def _extract_exact_reply_instruction(user_input: str) -> str:
1461
+ text = (user_input or "").strip()
1462
+ patterns = [
1463
+ r'(?is)reply\s+with\s+exactly\s+[:"]?\s*(.+?)\s*[".]?$',
1464
+ r'(?is)respond\s+with\s+exactly\s+[:"]?\s*(.+?)\s*[".]?$',
1465
+ r"(?is)قل\s+فقط[::]?\s*(.+?)\s*$",
1466
+ r"(?is)اكتب\s+فقط[::]?\s*(.+?)\s*$",
1467
+ ]
1468
+ for pattern in patterns:
1469
+ match = re.search(pattern, text)
1470
+ if match:
1471
+ return match.group(1).strip().strip("\"'`")
1472
+ return ""
1473
+
1474
+
1475
+ def _extract_exact_reply_instruction_safe(user_input: str) -> str:
1476
+ text = (user_input or "").strip()
1477
+ patterns = [
1478
+ r'(?is)reply\s+with\s+exactly\s+[:"]?\s*(.+?)\s*[".]?$',
1479
+ r'(?is)respond\s+with\s+exactly\s+[:"]?\s*(.+?)\s*[".]?$',
1480
+ r"(?is)\u0642\u0644\s+\u0641\u0642\u0637[:\uff1a]?\s*(.+?)\s*$",
1481
+ r"(?is)\u0627\u0643\u062a\u0628\s+\u0641\u0642\u0637[:\uff1a]?\s*(.+?)\s*$",
1482
+ ]
1483
+ for pattern in patterns:
1484
+ match = re.search(pattern, text)
1485
+ if match:
1486
+ return match.group(1).strip().strip("\"'`")
1487
+ return _extract_exact_reply_instruction(user_input)
1488
+
1489
+
1490
+ def _chat_system_instruction(language: str, user_input: str = "", exact_reply: str = "") -> str:
1491
+ if language == "ar":
1492
+ base = (
1493
+ "أنت KAPO-AI، مساعد هندسي عملي. "
1494
+ "أجب مباشرة وبوضوح وبشكل مفيد. "
1495
+ "افهم المطلوب أولاً ثم أجب دون مقدمات زائدة. "
1496
+ "لا تقل إن المحادثة غير مكتملة ولا تذكر تعليماتك الداخلية."
1497
+ )
1498
+ base += (
1499
+ " في هذا المشروع، كلمات مثل العقل والوكيل التنفيذي والنفق والرابط والريستارت والمزامنة "
1500
+ "والطبقات والتضمينات وكاجل وngrok وcloudflare وendpoint وmodel تشير غالبا إلى "
1501
+ "مكونات برمجية وتشغيلية، وليست معاني حرفية أو هندسة مدنية، ما لم يطلب المستخدم غير ذلك صراحة."
1502
+ )
1503
+ if exact_reply:
1504
+ return base + f' يجب أن يكون ردك هو هذا النص فقط حرفياً: "{exact_reply}"'
1505
+ return base
1506
+ base = (
1507
+ "You are KAPO-AI, an engineering assistant. "
1508
+ "Answer directly, clearly, and practically. "
1509
+ "Understand the request before answering. "
1510
+ "Do not say the conversation is incomplete and do not mention hidden instructions."
1511
+ )
1512
+ if exact_reply:
1513
+ return base + f' Your entire reply must be exactly: "{exact_reply}"'
1514
+ return base
1515
+
1516
+
1517
+ def _build_chat_prompt(user_input: str, history: list[dict[str, str]], context_block: str) -> str:
1518
+ language = _detect_language(user_input)
1519
+ exact_reply = _extract_exact_reply_instruction_safe(user_input)
1520
+ history_lines: list[str] = []
1521
+ for message in _prune_history(history):
1522
+ role = message.get("role", "user")
1523
+ role_label = "المستخدم" if language == "ar" and role == "user" else role.upper()
1524
+ if language == "ar" and role != "user":
1525
+ role_label = "المساعد"
1526
+ history_lines.append(f"{role_label}: {message.get('content', '')}")
1527
+
1528
+ history_section = ""
1529
+ if history_lines:
1530
+ history_section = "\n### History\n" + "\n".join(history_lines) + "\n"
1531
+
1532
+ context_section = f"\n### Context\n{context_block}\n" if context_block else ""
1533
+ user_label = "المستخدم" if language == "ar" else "User"
1534
+ assistant_label = "المساعد" if language == "ar" else "Assistant"
1535
+ return (
1536
+ f"### System\n{_chat_system_instruction(language, user_input, exact_reply)}\n"
1537
+ f"{history_section}"
1538
+ f"{context_section}"
1539
+ f"### Instruction\n"
1540
+ f"{user_label}: {user_input}\n"
1541
+ f"{assistant_label}:"
1542
+ )
1543
+
1544
+
1545
+ def _response_looks_bad(text: str, language: str) -> bool:
1546
+ cleaned = (text or "").strip()
1547
+ if not cleaned:
1548
+ return True
1549
+ markers = [
1550
+ "the assistant is not sure",
1551
+ "conversation seems incomplete",
1552
+ "provide more information",
1553
+ "unless otherwise noted",
1554
+ "as an ai model developed by",
1555
+ "developed by ibm",
1556
+ "tensorflow library",
1557
+ "dataset of 1024",
1558
+ ]
1559
+ if any(marker in cleaned.lower() for marker in markers):
1560
+ return True
1561
+ if language == "ar":
1562
+ arabic_chars = len(re.findall(r"[\u0600-\u06FF]", cleaned))
1563
+ latin_chars = len(re.findall(r"[A-Za-z]", cleaned))
1564
+ if arabic_chars < 8 and latin_chars > max(12, arabic_chars * 2):
1565
+ return True
1566
+ return False
1567
+
1568
+
1569
+ def _fallback_response(user_input: str) -> str:
1570
+ if _detect_language(user_input) == "ar":
1571
+ return "فهمت رسالتك، لكن الرد المولد لم يكن صالحاً للاستخدام. أعد صياغة الطلب بشكل أكثر تحديداً."
1572
+ return "I understood your message, but the generated reply was not usable. Please rephrase the request more specifically."
1573
+
1574
+
1575
+ def _project_specific_fast_reply(user_input: str) -> str:
1576
+ text = (user_input or "").strip()
1577
+ lower = text.lower()
1578
+ if any(token in text for token in ("ايه اللي اتصلح", "إيه اللي اتصلح", "هو ايه اللي اتصلح", "هو إيه اللي اتصلح")) and any(
1579
+ token in text or token in lower for token in ("النفق", "الرابط", "الريستارت", "restart")
1580
+ ):
1581
+ return (
1582
+ "الذي اتصلح هو أن الريستارت الداخلي بقى يعيد تشغيل خدمة العقل نفسها من غير ما يكسر النفق أو يغيّر الرابط العام. "
1583
+ "ولو ظهر توقف قصير أثناء الإقلاع فهذا يكون من رجوع الخدمة، لا من إنشاء نفق جديد."
1584
+ )
1585
+ if "النفق" in text and any(token in text for token in ("إصلاح", "اصلاح", "الذي تم", "اتصلح", "تم إصلاح", "تم اصلاح")):
1586
+ return (
1587
+ "تم فصل دورة حياة النفق عن دورة حياة خدمة العقل، فأصبح ngrok يعمل كعامل مستقل عن عملية Uvicorn. "
1588
+ "وبالتالي يحتفظ /system/restart بنفس الرابط العام بدل إنشاء رابط جديد، وقد يظهر ERR_NGROK_8012 مؤقتًا فقط أثناء الإقلاع."
1589
+ )
1590
+ if "نفس الرابط" in text and ("ريستارت" in text or "restart" in lower):
1591
+ return (
1592
+ "الهدف هنا أن تعيد الخدمة الإقلاع داخليًا مع الإبقاء على نفس الـ public URL. "
1593
+ "لذلك يبقى tunnel حيًا، بينما تعود خدمة localhost:7860 للعمل بعد ثوانٍ قليلة على نفس الرابط."
1594
+ )
1595
+ return ""
1596
+
1597
+
1598
+ def _generate_response(user_input: str, history: list[dict[str, str]], context_block: str) -> str:
1599
+ language = _detect_language(user_input)
1600
+ exact_reply = _extract_exact_reply_instruction_safe(user_input)
1601
+ if exact_reply:
1602
+ return exact_reply
1603
+ fast_reply = _project_specific_fast_reply(user_input)
1604
+ if fast_reply:
1605
+ return fast_reply
1606
+ if MODEL is None:
1607
+ if language == "ar":
1608
+ return "الخدمة تعمل لكن توليد الرد الحر غير متاح الآن لأن النموذج غير محمل."
1609
+ return "The Brain is online, but natural chat generation is unavailable because the model is not loaded."
1610
+
1611
+ prompt = _build_chat_prompt(user_input, history, context_block)
1612
+
1613
+ try:
1614
+ max_tokens = 80 if language == "ar" else 96
1615
+ output = MODEL(
1616
+ prompt,
1617
+ max_tokens=max_tokens,
1618
+ temperature=0.1,
1619
+ top_p=0.85,
1620
+ stop=["\nUser:", "\nUSER:", "\nالمستخدم:", "\n###", "<|EOT|>"],
1621
+ )
1622
+ text = output["choices"][0]["text"].strip()
1623
+ if _response_looks_bad(text, language):
1624
+ return _fallback_response(user_input)
1625
+ return text or ("تم استلام رسالتك." if language == "ar" else "I received your message.")
1626
+ except Exception:
1627
+ logger.exception("Model generation failed")
1628
+ if language == "ar":
1629
+ return "فهمت طلبك، لكن فشل توليد الرد النصي."
1630
+ return "I understood your request, but text generation failed."
1631
+
1632
+
1633
+ def _store_chat_trace(request_id: str, payload: dict[str, Any]) -> None:
1634
+ executor_url = os.getenv("EXECUTOR_URL", "").strip().rstrip("/")
1635
+ if not _executor_roundtrip_allowed("BRAIN_REMOTE_TRACE_STORE_ENABLED", default=True):
1636
+ return
1637
+ try:
1638
+ requests.post(
1639
+ f"{executor_url}/memory/store",
1640
+ json={"request_id": request_id, "payload": payload},
1641
+ headers=_brain_headers(),
1642
+ timeout=(
1643
+ _executor_connect_timeout(),
1644
+ _executor_read_timeout("BRAIN_REMOTE_TRACE_STORE_TIMEOUT_SEC", 2.5),
1645
+ ),
1646
+ )
1647
+ except requests.exceptions.ReadTimeout:
1648
+ logger.info("Chat trace store timed out; continuing")
1649
+ except Exception:
1650
+ logger.warning("Failed to store chat trace on executor", exc_info=True)
1651
+
1652
+
1653
+ def _ingest_chat_knowledge(request_id: str, user_input: str, reply: str) -> None:
1654
+ if len(reply or "") < 180:
1655
+ return
1656
+ executor_url = os.getenv("EXECUTOR_URL", "").strip().rstrip("/")
1657
+ if not _executor_roundtrip_allowed("BRAIN_REMOTE_AUTO_INGEST_ENABLED", default=False):
1658
+ return
1659
+ payload = {
1660
+ "request_id": request_id,
1661
+ "payload": {
1662
+ "source": "auto_chat",
1663
+ "content": f"User: {user_input}\nAssistant: {reply}",
1664
+ },
1665
+ }
1666
+ try:
1667
+ requests.post(
1668
+ f"{executor_url}/rag/ingest",
1669
+ json=payload,
1670
+ headers=_brain_headers(),
1671
+ timeout=(
1672
+ _executor_connect_timeout(),
1673
+ _executor_read_timeout("BRAIN_REMOTE_AUTO_INGEST_TIMEOUT_SEC", 3.0),
1674
+ ),
1675
+ )
1676
+ except requests.exceptions.ReadTimeout:
1677
+ logger.info("Auto-ingest chat knowledge timed out; continuing")
1678
+ except Exception:
1679
+ logger.warning("Failed to auto-ingest chat knowledge", exc_info=True)
1680
+
1681
+
1682
+ def _learn_user_style(request_id: str, user_input: str, reply: str, context: dict[str, Any]) -> None:
1683
+ executor_url = os.getenv("EXECUTOR_URL", "").strip().rstrip("/")
1684
+ if not executor_url or not _executor_roundtrip_allowed("BRAIN_REMOTE_STYLE_PROFILE_ENABLED", default=False):
1685
+ return
1686
+ try:
1687
+ requests.post(
1688
+ f"{executor_url}/preferences/learn",
1689
+ json={
1690
+ "request_id": request_id,
1691
+ "user_input": user_input,
1692
+ "assistant_reply": reply,
1693
+ "context": context or {},
1694
+ },
1695
+ headers=_brain_headers(),
1696
+ timeout=(
1697
+ _executor_connect_timeout(),
1698
+ _executor_read_timeout("BRAIN_REMOTE_TRACE_STORE_TIMEOUT_SEC", 2.5),
1699
+ ),
1700
+ )
1701
+ except requests.exceptions.ReadTimeout:
1702
+ logger.info("Style learning timed out; continuing")
1703
+ except Exception:
1704
+ logger.warning("Failed to learn user style on executor", exc_info=True)
1705
+
1706
+
1707
+ def _dispatch_background(task, *args) -> None:
1708
+ try:
1709
+ threading.Thread(target=task, args=args, daemon=True).start()
1710
+ except Exception:
1711
+ logger.warning("Background task dispatch failed", exc_info=True)
1712
+
1713
+
1714
+ def _restart_process(delay_sec: float = 1.0) -> None:
1715
+ if _is_hf_space_runtime():
1716
+ logger.info("Skipping in-process restart on Hugging Face Space runtime")
1717
+ return
1718
+ def _run() -> None:
1719
+ time.sleep(max(0.2, float(delay_sec)))
1720
+ target_root = _sync_target_root()
1721
+ os.chdir(target_root)
1722
+ os.environ["KAPO_INTERNAL_RESTART"] = "1"
1723
+ os.environ.setdefault("KAPO_FAST_INTERNAL_RESTART", "1")
1724
+ if _reuse_public_url_on_restart():
1725
+ current_public_url = _load_saved_public_url()
1726
+ if current_public_url:
1727
+ _remember_public_url(current_public_url)
1728
+ os.environ["KAPO_RESTART_REUSE_PUBLIC_URL"] = "1"
1729
+ port = str(os.getenv("BRAIN_PORT", "7860") or "7860")
1730
+ app_module = str(os.getenv("KAPO_UVICORN_APP") or "").strip()
1731
+ if not app_module:
1732
+ if (Path(target_root) / "brain_server" / "api" / "main.py").exists():
1733
+ app_module = "brain_server.api.main:app"
1734
+ elif (Path(target_root) / "api" / "main.py").exists():
1735
+ app_module = "api.main:app"
1736
+ else:
1737
+ app_module = "brain_server.api.main:app"
1738
+ os.execv(
1739
+ sys.executable,
1740
+ [
1741
+ sys.executable,
1742
+ "-m",
1743
+ "uvicorn",
1744
+ app_module,
1745
+ "--host",
1746
+ "0.0.0.0",
1747
+ "--port",
1748
+ port,
1749
+ ],
1750
+ )
1751
+
1752
+ threading.Thread(target=_run, daemon=True).start()
1753
+
1754
+
1755
+ @app.get("/")
1756
+ async def root():
1757
+ return {"status": "ok", "service": "brain_server", "docs": "/docs", "health": "/health"}
1758
+
1759
+
1760
+ @app.get("/runtime/firebase")
1761
+ async def runtime_firebase_snapshot():
1762
+ try:
1763
+ return {"status": "ok", "firebase": _json_safe(_firebase_runtime_snapshot())}
1764
+ except Exception as exc:
1765
+ logger.warning("Failed to build firebase runtime snapshot", exc_info=True)
1766
+ return {"status": "degraded", "firebase": {"platforms": [], "models": [], "prompts": [], "roles": []}, "detail": str(exc)}
1767
+
1768
+
1769
+ @app.get("/runtime/errors")
1770
+ async def runtime_errors(limit: int = 50, level: str = "WARNING"):
1771
+ normalized = str(level or "WARNING").strip().upper()
1772
+ allowed = {"DEBUG": 10, "INFO": 20, "WARNING": 30, "ERROR": 40, "CRITICAL": 50}
1773
+ threshold = allowed.get(normalized, 30)
1774
+ items = [item for item in list(RUNTIME_LOG_BUFFER) if allowed.get(str(item.get("level") or "").upper(), 0) >= threshold]
1775
+ return {"status": "ok", "count": len(items[-limit:]), "items": items[-limit:]}
1776
+
1777
+
1778
+ @app.get("/runtime/modernization")
1779
+ async def runtime_modernization():
1780
+ try:
1781
+ return {"status": "ok", "modernization": _runtime_modernization_snapshot()}
1782
+ except Exception as exc:
1783
+ logger.warning("Failed to build runtime modernization snapshot", exc_info=True)
1784
+ return {"status": "degraded", "detail": str(exc), "modernization": {}}
1785
+
1786
+
1787
+ @app.get("/model/status")
1788
+ async def model_status():
1789
+ return {
1790
+ "loaded": MODEL is not None,
1791
+ "error": MODEL_ERROR,
1792
+ "repo_id": MODEL_META.get("repo_id"),
1793
+ "filename": MODEL_META.get("filename"),
1794
+ }
1795
+
1796
+
1797
+ @app.post("/model/load")
1798
+ async def model_load(req: ModelLoadRequest):
1799
+ ensure_model_loaded(req.repo_id, req.filename, hf_token=req.hf_token)
1800
+ return await model_status()
1801
+
1802
+
1803
+ @app.post("/model/hotswap")
1804
+ async def model_hotswap(req: ModelLoadRequest):
1805
+ global MODEL
1806
+ if MODEL is not None:
1807
+ del MODEL
1808
+ gc.collect()
1809
+ MODEL = None
1810
+ ensure_model_loaded(req.repo_id, req.filename, hf_token=req.hf_token)
1811
+ return await model_status()
1812
+
1813
+
1814
+ @app.post("/embeddings")
1815
+ async def embeddings(payload: dict[str, Any]):
1816
+ if EMBED_MODEL is None:
1817
+ _load_embed_model()
1818
+ texts = payload.get("texts") or []
1819
+ if not texts:
1820
+ return {"embeddings": []}
1821
+ return {"embeddings": EMBED_MODEL.encode(texts).tolist()}
1822
+
1823
+
1824
+ @app.post("/chat")
1825
+ async def chat(req: ChatRequest):
1826
+ try:
1827
+ exact_reply = _extract_exact_reply_instruction_safe(req.user_input)
1828
+ if exact_reply:
1829
+ runtime_context = {**(req.context or {}), "user_input": req.user_input}
1830
+ trace_payload = {
1831
+ "mode": "chat",
1832
+ "user_input": req.user_input,
1833
+ "reply": exact_reply,
1834
+ "plan": None,
1835
+ "execution": None,
1836
+ "knowledge": [],
1837
+ "web_results": [],
1838
+ "context": runtime_context,
1839
+ }
1840
+ memory = MemoryAgent()
1841
+ memory.write_short_term(req.request_id, trace_payload)
1842
+ return {
1843
+ "status": "ok",
1844
+ "mode": "chat",
1845
+ "reply": exact_reply,
1846
+ "plan": None,
1847
+ "rationale": None,
1848
+ "execution": None,
1849
+ "knowledge": [],
1850
+ "web_results": [],
1851
+ "timestamp": time.time(),
1852
+ }
1853
+
1854
+ planner = PlannerAgent()
1855
+ reasoning = ReasoningAgent()
1856
+ memory = MemoryAgent()
1857
+
1858
+ mode = "task" if _is_task_request(req.user_input) else "chat"
1859
+ runtime_context = {**(req.context or {}), "user_input": req.user_input}
1860
+ knowledge = _retrieve_knowledge(req.user_input, top_k=4) if _is_knowledge_request(req.user_input, req.context) else []
1861
+ web_results = _search_web(req.user_input) if _is_research_request(req.user_input) else []
1862
+ context_block = _append_runtime_instructions(_format_context_blocks(knowledge, web_results), runtime_context)
1863
+ response_text = ""
1864
+ plan_steps = None
1865
+ execution = None
1866
+ rationale = None
1867
+
1868
+ if mode == "task":
1869
+ plan_steps = planner.run(req.user_input, req.context)
1870
+ rationale = reasoning.run(req.user_input, plan_steps)
1871
+ response_text = (
1872
+ "سأتعامل مع هذه الرسالة كطلب تنفيذ، وقمت ببناء خطة مبدئية وسأبدأ التنفيذ تلقائياً."
1873
+ if _contains_arabic(req.user_input)
1874
+ else "I treated this as an execution request, built a plan, and started automatic execution."
1875
+ )
1876
+ if req.auto_execute:
1877
+ from api.routes_execute import ExecuteRequest, execute as execute_route
1878
+
1879
+ execution = await execute_route(
1880
+ ExecuteRequest(
1881
+ request_id=req.request_id,
1882
+ plan={"steps": plan_steps},
1883
+ executor_url=os.getenv("EXECUTOR_URL", "").strip() or None,
1884
+ )
1885
+ )
1886
+ if execution.get("report", {}).get("success"):
1887
+ response_text += (
1888
+ "\n\nتم التنفيذ بنجاح مبدئياً."
1889
+ if _contains_arabic(req.user_input)
1890
+ else "\n\nExecution completed successfully."
1891
+ )
1892
+ else:
1893
+ response_text += (
1894
+ "\n\nتمت المحاولة لكن توجد مخرجات تحتاج مراجعة."
1895
+ if _contains_arabic(req.user_input)
1896
+ else "\n\nExecution ran, but the result still needs review."
1897
+ )
1898
+ else:
1899
+ response_text = _generate_response(req.user_input, req.history, context_block)
1900
+
1901
+ trace_payload = {
1902
+ "mode": mode,
1903
+ "user_input": req.user_input,
1904
+ "reply": response_text,
1905
+ "plan": plan_steps,
1906
+ "execution": execution,
1907
+ "knowledge": knowledge,
1908
+ "web_results": web_results,
1909
+ "context": runtime_context,
1910
+ }
1911
+ memory.write_short_term(req.request_id, trace_payload)
1912
+ _dispatch_background(_store_chat_trace, req.request_id, trace_payload)
1913
+ _dispatch_background(_ingest_chat_knowledge, req.request_id, req.user_input, response_text)
1914
+ _dispatch_background(_learn_user_style, req.request_id, req.user_input, response_text, runtime_context)
1915
+
1916
+ return {
1917
+ "status": "ok",
1918
+ "mode": mode,
1919
+ "reply": response_text,
1920
+ "plan": plan_steps,
1921
+ "rationale": rationale,
1922
+ "execution": execution,
1923
+ "knowledge": knowledge,
1924
+ "web_results": web_results,
1925
+ "timestamp": time.time(),
1926
+ }
1927
+ except Exception as exc:
1928
+ logger.exception("Chat failed")
1929
+ return {
1930
+ "status": "error",
1931
+ "mode": "chat",
1932
+ "reply": "حدث خطأ أثناء معالجة الرسالة." if _contains_arabic(req.user_input) else "Chat processing failed.",
1933
+ "detail": str(exc),
1934
+ "timestamp": time.time(),
1935
+ }
1936
+
1937
+
1938
+ @app.post("/init-connection")
1939
+ async def init_connection(payload: ConnectionInit):
1940
+ _remember_executor_url(payload.executor_url)
1941
+ FIREBASE.set_document("runtime", "executor", {"executor_url": payload.executor_url})
1942
+ public_url = _report_known_public_url()
1943
+ if not public_url:
1944
+ public_url = start_ngrok(payload.ngrok_token)
1945
+ return {"status": "connected", "brain_public_url": public_url}
1946
+
1947
+
1948
+ @app.post("/system/publish-url")
1949
+ async def system_publish_url(req: PublishUrlRequest | None = None):
1950
+ payload = req or PublishUrlRequest()
1951
+ explicit_public_url = str(payload.public_url or "").strip().rstrip("/")
1952
+ if explicit_public_url:
1953
+ _remember_public_url(explicit_public_url)
1954
+ _report_brain_url(explicit_public_url)
1955
+ FIREBASE.set_document("brains", explicit_public_url, {"url": explicit_public_url, "status": "healthy", "source": "explicit_publish"})
1956
+ return {"status": "published", "brain_public_url": explicit_public_url, "mode": "explicit"}
1957
+
1958
+ if not payload.start_tunnel:
1959
+ public_url = _report_known_public_url()
1960
+ if public_url:
1961
+ return {"status": "published", "brain_public_url": public_url, "mode": "saved"}
1962
+ return {"status": "skipped", "brain_public_url": None, "mode": "none"}
1963
+
1964
+ public_url = start_ngrok(payload.ngrok_token)
1965
+ if public_url:
1966
+ return {"status": "published", "brain_public_url": public_url, "mode": "ngrok"}
1967
+ public_url = _report_known_public_url()
1968
+ return {"status": "published" if public_url else "error", "brain_public_url": public_url, "mode": "saved" if public_url else "none"}
1969
+
1970
+
1971
+ @app.get("/system/files")
1972
+ async def system_files(path: str = "", include_content: bool = False):
1973
+ try:
1974
+ target = _resolve_sync_path(path)
1975
+ if not target.exists():
1976
+ return {"status": "error", "detail": "Path not found", "path": path}
1977
+ if target.is_file():
1978
+ payload = {"status": "ok", "entry": _describe_sync_entry(target)}
1979
+ if include_content:
1980
+ payload["content"] = target.read_text(encoding="utf-8", errors="ignore")
1981
+ return payload
1982
+ items = sorted((_describe_sync_entry(item) for item in target.iterdir()), key=lambda item: (not item["is_dir"], item["name"].lower()))
1983
+ return {"status": "ok", "root": str(_sync_root_path()), "path": path, "items": items}
1984
+ except Exception as exc:
1985
+ logger.exception("File listing failed")
1986
+ return {"status": "error", "detail": str(exc), "path": path}
1987
+
1988
+
1989
+ @app.post("/system/files/write")
1990
+ async def system_files_write(payload: FileWriteRequest):
1991
+ try:
1992
+ target = _resolve_sync_path(payload.path)
1993
+ target.parent.mkdir(parents=True, exist_ok=True)
1994
+ if target.exists() and target.is_dir():
1995
+ return {"status": "error", "detail": "Target path is a directory"}
1996
+ if target.exists() and not payload.overwrite:
1997
+ return {"status": "error", "detail": "File already exists"}
1998
+ target.write_text(payload.content or "", encoding="utf-8")
1999
+ return {"status": "saved", "entry": _describe_sync_entry(target)}
2000
+ except Exception as exc:
2001
+ logger.exception("File write failed")
2002
+ return {"status": "error", "detail": str(exc), "path": payload.path}
2003
+
2004
+
2005
+ @app.post("/system/files/mkdir")
2006
+ async def system_files_mkdir(payload: FileMkdirRequest):
2007
+ try:
2008
+ target = _resolve_sync_path(payload.path)
2009
+ target.mkdir(parents=True, exist_ok=True)
2010
+ return {"status": "created", "entry": _describe_sync_entry(target)}
2011
+ except Exception as exc:
2012
+ logger.exception("Directory creation failed")
2013
+ return {"status": "error", "detail": str(exc), "path": payload.path}
2014
+
2015
+
2016
+ @app.delete("/system/files")
2017
+ async def system_files_delete(payload: FileDeleteRequest):
2018
+ try:
2019
+ target = _resolve_sync_path(payload.path)
2020
+ if not target.exists():
2021
+ return {"status": "deleted", "path": payload.path, "existed": False}
2022
+ if target.is_dir():
2023
+ if payload.recursive:
2024
+ shutil.rmtree(target)
2025
+ else:
2026
+ target.rmdir()
2027
+ else:
2028
+ target.unlink()
2029
+ return {"status": "deleted", "path": payload.path, "existed": True}
2030
+ except Exception as exc:
2031
+ logger.exception("Delete failed")
2032
+ return {"status": "error", "detail": str(exc), "path": payload.path}
2033
+
2034
+
2035
+ if HAS_MULTIPART:
2036
+ @app.post("/system/sync")
2037
+ async def sync_codebase(file: UploadFile = File(...), restart: bool = False):
2038
+ temp_zip = os.path.join(tempfile.gettempdir(), "kapo_update.zip")
2039
+ try:
2040
+ with open(temp_zip, "wb") as buffer:
2041
+ shutil.copyfileobj(file.file, buffer)
2042
+ with zipfile.ZipFile(temp_zip, "r") as zip_ref:
2043
+ zip_ref.extractall(_sync_target_root())
2044
+ if restart:
2045
+ _restart_process()
2046
+ return {"status": "synced", "target_root": _sync_target_root(), "restart_scheduled": restart}
2047
+ except Exception as exc:
2048
+ logger.exception("Code sync failed")
2049
+ return {"status": "error", "detail": str(exc)}
2050
+
2051
+ @app.post("/system/archive/upload")
2052
+ async def system_archive_upload(file: UploadFile = File(...), target_path: str = "", restart: bool = False):
2053
+ temp_zip = os.path.join(tempfile.gettempdir(), "kapo_archive_upload.zip")
2054
+ try:
2055
+ extract_root = _resolve_sync_path(target_path)
2056
+ extract_root.mkdir(parents=True, exist_ok=True)
2057
+ with open(temp_zip, "wb") as buffer:
2058
+ shutil.copyfileobj(file.file, buffer)
2059
+ with zipfile.ZipFile(temp_zip, "r") as zip_ref:
2060
+ zip_ref.extractall(extract_root)
2061
+ if restart:
2062
+ _restart_process()
2063
+ return {
2064
+ "status": "extracted",
2065
+ "target_root": str(extract_root),
2066
+ "restart_scheduled": restart,
2067
+ }
2068
+ except Exception as exc:
2069
+ logger.exception("Archive upload failed")
2070
+ return {"status": "error", "detail": str(exc)}
2071
+ else:
2072
+ @app.post("/system/sync")
2073
+ async def sync_codebase_unavailable():
2074
+ return {"status": "error", "detail": "python-multipart is required for /system/sync"}
2075
+
2076
+ @app.post("/system/archive/upload")
2077
+ async def system_archive_upload_unavailable():
2078
+ return {"status": "error", "detail": "python-multipart is required for /system/archive/upload"}
2079
+
2080
+
2081
+ @app.post("/system/restart")
2082
+ async def system_restart(req: RestartRequest | None = None):
2083
+ delay_sec = req.delay_sec if req else 1.0
2084
+ if _is_hf_space_runtime():
2085
+ return {
2086
+ "status": "skipped",
2087
+ "reason": "restart_disabled_on_hf_space",
2088
+ "delay_sec": delay_sec,
2089
+ "target_root": _sync_target_root(),
2090
+ }
2091
+ _restart_process(delay_sec=delay_sec)
2092
+ return {
2093
+ "status": "restarting",
2094
+ "delay_sec": delay_sec,
2095
+ "target_root": _sync_target_root(),
2096
+ }
2097
+
2098
+
2099
+ def _health_payload(check_executor: bool = False, executor_url: str | None = None) -> dict[str, Any]:
2100
+ cfg = load_config()
2101
+ base_exec_url = (executor_url or os.getenv("EXECUTOR_URL", "")).strip().rstrip("/")
2102
+ exec_ok = False
2103
+ exec_checked = False
2104
+ exec_error = None
2105
+ health_timeout = int(cfg.get("REQUEST_TIMEOUT_SEC", 20) or 20)
2106
+ health_retries = max(1, int(cfg.get("REQUEST_RETRIES", 2) or 2))
2107
+
2108
+ if base_exec_url and check_executor:
2109
+ exec_checked = True
2110
+ for _ in range(health_retries):
2111
+ try:
2112
+ response = requests.get(
2113
+ f"{base_exec_url}/health",
2114
+ headers=get_executor_headers(cfg),
2115
+ timeout=health_timeout,
2116
+ )
2117
+ exec_ok = response.status_code == 200
2118
+ if exec_ok:
2119
+ exec_error = None
2120
+ break
2121
+ exec_error = response.text
2122
+ except Exception as exc:
2123
+ exec_error = str(exc)
2124
+
2125
+ faiss_path = cfg.get("FAISS_INDEX_PATH")
2126
+ return {
2127
+ "status": "ok",
2128
+ "model_loaded": MODEL is not None,
2129
+ "model_error": MODEL_ERROR,
2130
+ "embedding_loaded": EMBED_MODEL is not None,
2131
+ "faiss_ok": bool(faiss_path and os.path.exists(faiss_path)),
2132
+ "executor_checked": exec_checked,
2133
+ "executor_ok": exec_ok,
2134
+ "executor_error": exec_error,
2135
+ "remote_brain_only": str(os.getenv("REMOTE_BRAIN_ONLY", "")).strip().lower() in {"1", "true", "yes", "on"},
2136
+ "runtime_root": os.getenv("KAPO_RUNTIME_ROOT", ""),
2137
+ "sync_root": _sync_target_root(),
2138
+ "timestamp": time.time(),
2139
+ }
2140
+
2141
+
2142
+ def _decode_b64_text(value: str) -> str:
2143
+ raw = str(value or "").strip()
2144
+ if not raw:
2145
+ return ""
2146
+ padding = "=" * (-len(raw) % 4)
2147
+ try:
2148
+ return base64.urlsafe_b64decode((raw + padding).encode("utf-8")).decode("utf-8").strip()
2149
+ except Exception:
2150
+ return ""
2151
+
2152
+
2153
+ def _runtime_modernization_snapshot() -> dict[str, Any]:
2154
+ bootstrap = DRIVE_STATE.ensure_bootstrap_loaded(force=False) or {}
2155
+ patch_manifest_url = str(
2156
+ os.getenv("KAPO_PATCH_MANIFEST_URL", "")
2157
+ or bootstrap.get("patch_manifest_url")
2158
+ or ""
2159
+ ).strip()
2160
+ remote_env_url = _decode_b64_text(os.getenv("KAPO_REMOTE_ENV_URL_B64", "")) or str(os.getenv("KAPO_REMOTE_ENV_URL", "") or "").strip()
2161
+ public_url = str(os.getenv("BRAIN_PUBLIC_URL", "") or LAST_BRAIN_URL_REPORT.get("url") or _load_saved_public_url() or "").strip()
2162
+ applied_version = _load_applied_runtime_version()
2163
+ control_plane_url = str(os.getenv("KAPO_CONTROL_PLANE_URL", "") or "").strip()
2164
+ queue_name = str(os.getenv("KAPO_CLOUDFLARE_QUEUE_NAME", "") or "").strip()
2165
+ return {
2166
+ "shared_state_backend": _shared_state_backend(),
2167
+ "firebase_enabled": str(os.getenv("FIREBASE_ENABLED", "0")).strip().lower() in {"1", "true", "yes", "on"},
2168
+ "drive": {
2169
+ "folder_id": str(os.getenv("GOOGLE_DRIVE_SHARED_STATE_FOLDER_ID", "") or os.getenv("GOOGLE_DRIVE_STORAGE_FOLDER_ID", "") or "").strip(),
2170
+ "prefix": str(os.getenv("GOOGLE_DRIVE_SHARED_STATE_PREFIX", "") or os.getenv("GOOGLE_DRIVE_STORAGE_PREFIX", "") or "").strip(),
2171
+ "bootstrap_loaded": bool(bootstrap),
2172
+ },
2173
+ "bootstrap": {
2174
+ "configured": bool(str(os.getenv("KAPO_BOOTSTRAP_URL", "") or os.getenv("GOOGLE_DRIVE_BOOTSTRAP_URL", "") or "").strip()),
2175
+ "url": str(os.getenv("KAPO_BOOTSTRAP_URL", "") or os.getenv("GOOGLE_DRIVE_BOOTSTRAP_URL", "") or "").strip(),
2176
+ "loaded": bool(bootstrap),
2177
+ "version": str(bootstrap.get("version") or "").strip(),
2178
+ "updated_at": bootstrap.get("updated_at"),
2179
+ },
2180
+ "patch": {
2181
+ "configured": bool(patch_manifest_url),
2182
+ "manifest_url": patch_manifest_url,
2183
+ "startup_self_update_enabled": _startup_self_update_enabled(),
2184
+ "applied_version": applied_version,
2185
+ },
2186
+ "remote_env": {
2187
+ "configured": bool(remote_env_url),
2188
+ "loaded": str(os.getenv("KAPO_REMOTE_ENV_LOADED", "0")).strip().lower() in {"1", "true", "yes", "on"},
2189
+ "url": remote_env_url,
2190
+ },
2191
+ "control_plane": {
2192
+ "configured": bool(control_plane_url),
2193
+ "url": control_plane_url,
2194
+ "queue_name": queue_name,
2195
+ "queue_configured": bool(queue_name),
2196
+ },
2197
+ "transport": {
2198
+ "provider": str(os.getenv("BRAIN_TUNNEL_PROVIDER", "ngrok") or "ngrok").strip().lower(),
2199
+ "auto_ngrok": _ngrok_bootstrap_enabled(),
2200
+ "reuse_public_url_on_restart": _reuse_public_url_on_restart(),
2201
+ "public_url": public_url,
2202
+ "executor_url": str(os.getenv("EXECUTOR_URL", "") or "").strip(),
2203
+ },
2204
+ "runtime": {
2205
+ "root": str(_sync_target_root()),
2206
+ "model_profile_id": str(os.getenv("MODEL_PROFILE_ID", "") or "").strip(),
2207
+ "model_loaded": MODEL is not None,
2208
+ "embed_loaded": EMBED_MODEL is not None,
2209
+ },
2210
+ }
2211
+
2212
+
2213
+ def _persist_runtime_state_snapshot(reason: str = "periodic") -> dict[str, Any]:
2214
+ payload = _health_payload(check_executor=False)
2215
+ public_url = os.getenv("BRAIN_PUBLIC_URL") or _load_saved_public_url() or ""
2216
+ state_id = _brain_state_id()
2217
+ FIREBASE.set_document(
2218
+ "brains",
2219
+ public_url or state_id,
2220
+ {
2221
+ "url": public_url,
2222
+ "health": payload,
2223
+ "status": "healthy" if payload["model_loaded"] else "degraded",
2224
+ "model_repo": os.getenv("MODEL_REPO", DEFAULT_MODEL_REPO),
2225
+ "model_file": os.getenv("MODEL_FILE", DEFAULT_MODEL_FILE),
2226
+ "model_profile_id": os.getenv("MODEL_PROFILE_ID", DEFAULT_MODEL_PROFILE_ID),
2227
+ "roles": os.getenv("BRAIN_ROLES", "supervisor,chat,coding,planner,arabic,fallback"),
2228
+ "languages": os.getenv("BRAIN_LANGUAGES", "ar,en"),
2229
+ "updated_at": time.time(),
2230
+ "source": reason,
2231
+ },
2232
+ min_interval_sec=5.0,
2233
+ )
2234
+ FIREBASE.set_document(
2235
+ "brain_runtime",
2236
+ state_id,
2237
+ {
2238
+ "brain_url": public_url,
2239
+ "reason": reason,
2240
+ "health": payload,
2241
+ "recent_logs": list(RUNTIME_LOG_BUFFER)[-25:],
2242
+ "updated_at": time.time(),
2243
+ },
2244
+ min_interval_sec=5.0,
2245
+ )
2246
+ return payload
2247
+
2248
+
2249
+ def _runtime_state_pulse() -> None:
2250
+ interval = max(20.0, float(os.getenv("BRAIN_STATE_SYNC_INTERVAL_SEC", "60") or 60))
2251
+ while True:
2252
+ try:
2253
+ _persist_runtime_state_snapshot(reason="background_pulse")
2254
+ except Exception:
2255
+ logger.warning("Background runtime state sync failed", exc_info=True)
2256
+ time.sleep(interval)
2257
+
2258
+
2259
+ @app.get("/health")
2260
+ async def health(executor_url: str | None = None, check_executor: bool = False):
2261
+ payload = _health_payload(check_executor=check_executor, executor_url=executor_url)
2262
+ _persist_runtime_state_snapshot(reason="health_endpoint")
2263
+ return payload
2264
+
2265
+
2266
+ # KAPO HF SPACE TRANSFORMERS PATCH
2267
+ def _kapo_hf_transformers_enabled() -> bool:
2268
+ return str(os.getenv('KAPO_HF_TRANSFORMERS_RUNTIME', '0')).strip().lower() in {'1', 'true', 'yes', 'on'}
2269
+
2270
+ def ensure_model_loaded(repo_id: str, filename: str, hf_token: str | None = None) -> None:
2271
+ global MODEL, MODEL_ERROR, MODEL_META
2272
+ repo_id = (repo_id or '').strip()
2273
+ filename = (filename or '').strip()
2274
+ if not repo_id:
2275
+ MODEL = None
2276
+ MODEL_ERROR = 'model repo missing'
2277
+ return
2278
+ if _kapo_hf_transformers_enabled():
2279
+ try:
2280
+ from transformers import AutoModelForCausalLM, AutoTokenizer
2281
+ tokenizer = AutoTokenizer.from_pretrained(repo_id, token=hf_token, trust_remote_code=True)
2282
+ model = AutoModelForCausalLM.from_pretrained(repo_id, token=hf_token, trust_remote_code=True, device_map='cpu')
2283
+ if hasattr(model, 'eval'):
2284
+ model.eval()
2285
+ MODEL = {'kind': 'transformers', 'model': model, 'tokenizer': tokenizer}
2286
+ MODEL_ERROR = None
2287
+ MODEL_META = {'repo_id': repo_id, 'filename': filename, 'path': None}
2288
+ logger.info('Loaded transformers model %s', repo_id)
2289
+ return
2290
+ except Exception as exc:
2291
+ MODEL = None
2292
+ MODEL_ERROR = f'transformers model load failed: {exc}'
2293
+ logger.exception('Transformers model load failed')
2294
+ return
2295
+ if not filename:
2296
+ MODEL = None
2297
+ MODEL_ERROR = 'model file missing'
2298
+ return
2299
+ try:
2300
+ model_path = _download_model(repo_id, filename, hf_token=hf_token)
2301
+ except Exception as exc:
2302
+ MODEL = None
2303
+ MODEL_ERROR = f'model download failed: {exc}'
2304
+ logger.exception('Model download failed')
2305
+ return
2306
+ try:
2307
+ from llama_cpp import Llama
2308
+ MODEL = Llama(model_path=model_path, n_ctx=4096)
2309
+ MODEL_ERROR = None
2310
+ MODEL_META = {'repo_id': repo_id, 'filename': filename, 'path': model_path}
2311
+ logger.info('Loaded model %s/%s', repo_id, filename)
2312
+ except Exception as exc:
2313
+ MODEL = None
2314
+ MODEL_ERROR = f'model load failed: {exc}'
2315
+ logger.exception('Model load failed')
2316
+
2317
+ def _generate_response(user_input: str, history: list[dict[str, str]], context_block: str) -> str:
2318
+ language = _detect_language(user_input)
2319
+ exact_reply = _extract_exact_reply_instruction_safe(user_input)
2320
+ if exact_reply:
2321
+ return exact_reply
2322
+ fast_reply = _project_specific_fast_reply(user_input)
2323
+ if fast_reply:
2324
+ return fast_reply
2325
+ if MODEL is None:
2326
+ try:
2327
+ _load_default_model()
2328
+ except Exception:
2329
+ logger.exception('Lazy model load failed')
2330
+ if MODEL is None:
2331
+ if language == 'ar':
2332
+ return 'الخدمة تعمل لكن توليد الرد الحر غير متاح الآن لأن النموذج غير محمل.'
2333
+ return 'The Brain is online, but natural chat generation is unavailable because the model is not loaded.'
2334
+ prompt = _build_chat_prompt(user_input, history, context_block)
2335
+ try:
2336
+ max_tokens = 80 if language == 'ar' else 96
2337
+ if isinstance(MODEL, dict) and MODEL.get('kind') == 'transformers':
2338
+ tokenizer = MODEL['tokenizer']
2339
+ model = MODEL['model']
2340
+ inputs = tokenizer(prompt, return_tensors='pt', truncation=True, max_length=2048)
2341
+ if hasattr(model, 'device'):
2342
+ inputs = {k: v.to(model.device) if hasattr(v, 'to') else v for k, v in inputs.items()}
2343
+ output_ids = model.generate(**inputs, max_new_tokens=max_tokens, do_sample=False, pad_token_id=tokenizer.eos_token_id)
2344
+ generated = output_ids[0][inputs['input_ids'].shape[1]:]
2345
+ text = tokenizer.decode(generated, skip_special_tokens=True).strip()
2346
+ else:
2347
+ output = MODEL(prompt, max_tokens=max_tokens, temperature=0.1, top_p=0.85, stop=['\nUser:', '\nUSER:', '\n###', '<|EOT|>'])
2348
+ text = output['choices'][0]['text'].strip()
2349
+ if _response_looks_bad(text, language):
2350
+ return _fallback_response(user_input)
2351
+ return text or ('تم استلام رسالتك.' if language == 'ar' else 'I received your message.')
2352
+ except Exception:
2353
+ logger.exception('Model generation failed')
2354
+ if language == 'ar':
2355
+ return 'فهمت طلبك، لكن حدث خطأ أثناء توليد الرد النصي.'
2356
+ 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,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ websockets==12.0
19
+
20
+ sentence-transformers
21
+ transformers
22
+
23
+ peft
24
+ bitsandbytes
25
+ trl
26
+ accelerate
27
+ datasets
kapo.env ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ BRAIN_AUTO_NGROK=0
2
+ BRAIN_AUTO_PUBLISH_URL_ON_STARTUP=0
3
+ BRAIN_LANGUAGES=ar,en
4
+ BRAIN_PLATFORM_NAME=ai_coder_main
5
+ BRAIN_PROVIDER=huggingface
6
+ BRAIN_REUSE_PUBLIC_URL_ON_RESTART=0
7
+ BRAIN_ROLES=coding,planner,fallback
8
+ BRAIN_TEMPLATE=hf-space-cpu
9
+ BRAIN_TUNNEL_PROVIDER=none
10
+ FIREBASE_ENABLED=0
11
+ FIREBASE_NAMESPACE=kapo
12
+ FIREBASE_PROJECT_ID=citadel4travels
13
+ GOOGLE_DRIVE_BOOTSTRAP_URL=https://drive.google.com/uc?export=download&id=19jyBWsQ9ciJVPi2PUigu5ti3gJ24A6TG
14
+ HF_ACCELERATOR=cpu
15
+ HF_SPACE_DOCKER=1
16
+ KAGGLE_AUTO_BOOTSTRAP=0
17
+ KAPO_BOOTSTRAP_URL=https://drive.google.com/uc?export=download&id=19jyBWsQ9ciJVPi2PUigu5ti3gJ24A6TG
18
+ KAPO_COMPUTE_PROFILE=cpu
19
+ KAPO_HF_INFERENCE_API=1
20
+ KAPO_HF_TRANSFORMERS_RUNTIME=0
21
+ KAPO_LAZY_EMBED_STARTUP=1
22
+ KAPO_LAZY_MODEL_STARTUP=1
23
+ KAPO_PATCH_BUNDLE_URL=https://drive.google.com/uc?export=download&id=16rIe05GZihhAz7ba8E-WibKaJKbh9eu1
24
+ KAPO_PATCH_MANIFEST_URL=https://drive.google.com/uc?export=download&id=1jLuPMCA3hp9qstZZtpBNzTK0XOmLrV8b
25
+ KAPO_REMOTE_ENV_PASSWORD_B64=cENvSnljQ0Z6RUN2azRpcUFLVUdQLW9BbVBoTEtNOFE
26
+ KAPO_REMOTE_ENV_URL_B64=aHR0cHM6Ly9kcml2ZS5nb29nbGUuY29tL3VjP2V4cG9ydD1kb3dubG9hZCZpZD0xdjlZRDlNYlBKNUdYNk9WanVHM3hOR05xLWJfQlJfTGY
27
+ KAPO_SHARED_STATE_BACKEND=google_drive
28
+ MODEL_PROFILE_ID=hf-coder-qwen25-coder-7b-instruct
29
+ MODEL_REPO=Qwen/Qwen2.5-Coder-1.5B-Instruct
30
+ REMOTE_BRAIN_ONLY=1
31
+ SPACE_PUBLIC_URL=https://MrA7A1-AiCoder.hf.space
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ fastapi>=0.115.2,<1.0
2
+ uvicorn==0.30.1
3
+ pydantic==2.7.3
4
+ python-dotenv==1.0.1
5
+ PyYAML>=6.0,<7.0
6
+ requests==2.32.3
7
+ python-json-logger==2.0.7
8
+ huggingface_hub>=0.33.5,<2.0
9
+ starlette>=0.40.0,<1.0
shared/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Shared utilities for KAPO runtimes."""
shared/google_drive_state.py ADDED
@@ -0,0 +1,409 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Google Drive backed shared-state utilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import json
7
+ import logging
8
+ import os
9
+ import sqlite3
10
+ import threading
11
+ import time
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ import requests
16
+
17
+
18
+ def _safe_name(value: str, default: str = "item") -> str:
19
+ text = str(value or "").strip() or default
20
+ return "".join(ch if ch.isalnum() or ch in {"-", "_", "."} else "_" for ch in text)[:180]
21
+
22
+
23
+ class GoogleDriveStateClient:
24
+ def __init__(self, logger: logging.Logger) -> None:
25
+ self.logger = logger
26
+ self._folder_cache: dict[str, str] = {}
27
+ self._file_cache: dict[str, str] = {}
28
+ self._bootstrap_lock = threading.Lock()
29
+ self._bootstrap_loaded_at = 0.0
30
+ self._bootstrap_payload: dict[str, Any] = {}
31
+
32
+ def _bootstrap_url(self) -> str:
33
+ return str(os.getenv("GOOGLE_DRIVE_BOOTSTRAP_URL", "") or os.getenv("KAPO_BOOTSTRAP_URL", "") or "").strip()
34
+
35
+ def _bootstrap_ttl(self) -> float:
36
+ return float(os.getenv("GOOGLE_DRIVE_BOOTSTRAP_TTL_SEC", "300") or 300)
37
+
38
+ def _apply_bootstrap_payload(self, payload: dict[str, Any]) -> None:
39
+ mappings = {
40
+ "shared_state_backend": "KAPO_SHARED_STATE_BACKEND",
41
+ "shared_state_folder_id": "GOOGLE_DRIVE_SHARED_STATE_FOLDER_ID",
42
+ "shared_state_prefix": "GOOGLE_DRIVE_SHARED_STATE_PREFIX",
43
+ "storage_folder_id": "GOOGLE_DRIVE_STORAGE_FOLDER_ID",
44
+ "storage_prefix": "GOOGLE_DRIVE_STORAGE_PREFIX",
45
+ "access_token": "GOOGLE_DRIVE_ACCESS_TOKEN",
46
+ "refresh_token": "GOOGLE_DRIVE_REFRESH_TOKEN",
47
+ "client_secret_json": "GOOGLE_DRIVE_CLIENT_SECRET_JSON",
48
+ "client_secret_json_base64": "GOOGLE_DRIVE_CLIENT_SECRET_JSON_BASE64",
49
+ "client_secret_path": "GOOGLE_DRIVE_CLIENT_SECRET_PATH",
50
+ "token_expires_at": "GOOGLE_DRIVE_TOKEN_EXPIRES_AT",
51
+ "firebase_enabled": "FIREBASE_ENABLED",
52
+ "executor_url": "EXECUTOR_URL",
53
+ "executor_public_url": "EXECUTOR_PUBLIC_URL",
54
+ "ngrok_authtoken": "NGROK_AUTHTOKEN",
55
+ "brain_public_url": "BRAIN_PUBLIC_URL",
56
+ "brain_roles": "BRAIN_ROLES",
57
+ "brain_languages": "BRAIN_LANGUAGES",
58
+ }
59
+ for key, env_name in mappings.items():
60
+ value = payload.get(key)
61
+ if value not in (None, ""):
62
+ os.environ[env_name] = str(value)
63
+
64
+ def ensure_bootstrap_loaded(self, force: bool = False) -> dict[str, Any]:
65
+ bootstrap_url = self._bootstrap_url()
66
+ if not bootstrap_url:
67
+ return {}
68
+ now = time.time()
69
+ if not force and self._bootstrap_payload and (now - self._bootstrap_loaded_at) < self._bootstrap_ttl():
70
+ return dict(self._bootstrap_payload)
71
+ with self._bootstrap_lock:
72
+ now = time.time()
73
+ if not force and self._bootstrap_payload and (now - self._bootstrap_loaded_at) < self._bootstrap_ttl():
74
+ return dict(self._bootstrap_payload)
75
+ try:
76
+ response = requests.get(bootstrap_url, timeout=20)
77
+ response.raise_for_status()
78
+ payload = dict(response.json() or {})
79
+ self._bootstrap_payload = payload
80
+ self._bootstrap_loaded_at = time.time()
81
+ self._apply_bootstrap_payload(payload)
82
+ return dict(payload)
83
+ except Exception:
84
+ self.logger.warning("Failed to load Google Drive bootstrap manifest", exc_info=True)
85
+ return dict(self._bootstrap_payload)
86
+
87
+ def enabled(self) -> bool:
88
+ self.ensure_bootstrap_loaded()
89
+ return bool(self.root_folder_id())
90
+
91
+ def root_folder_id(self) -> str:
92
+ return str(
93
+ os.getenv("GOOGLE_DRIVE_SHARED_STATE_FOLDER_ID", "")
94
+ or os.getenv("GOOGLE_DRIVE_STORAGE_FOLDER_ID", "")
95
+ or ""
96
+ ).strip()
97
+
98
+ def prefix(self) -> str:
99
+ return str(
100
+ os.getenv("GOOGLE_DRIVE_SHARED_STATE_PREFIX", "")
101
+ or os.getenv("GOOGLE_DRIVE_STORAGE_PREFIX", "")
102
+ or "kapo/shared_state"
103
+ ).strip("/ ")
104
+
105
+ def _control_plane_token(self, label: str) -> str:
106
+ candidates = [
107
+ Path.cwd().resolve() / "data" / "local" / "control_plane.db",
108
+ Path.cwd().resolve() / "data" / "drive_cache" / "control_plane.db",
109
+ ]
110
+ for db_path in candidates:
111
+ if not db_path.exists():
112
+ continue
113
+ try:
114
+ conn = sqlite3.connect(db_path)
115
+ conn.row_factory = sqlite3.Row
116
+ row = conn.execute(
117
+ "SELECT token FROM ngrok_tokens WHERE label = ? AND enabled = 1",
118
+ (label,),
119
+ ).fetchone()
120
+ conn.close()
121
+ token = str(row["token"]) if row else ""
122
+ if token:
123
+ return token
124
+ except Exception:
125
+ self.logger.warning("Failed to read Google Drive token from %s", db_path, exc_info=True)
126
+ return ""
127
+
128
+ def _client_secret_payload(self) -> dict[str, Any] | None:
129
+ raw = str(os.getenv("GOOGLE_DRIVE_CLIENT_SECRET_JSON", "")).strip()
130
+ raw_b64 = str(os.getenv("GOOGLE_DRIVE_CLIENT_SECRET_JSON_BASE64", "")).strip()
131
+ path = str(os.getenv("GOOGLE_DRIVE_CLIENT_SECRET_PATH", "")).strip()
132
+ if raw:
133
+ try:
134
+ return json.loads(raw)
135
+ except Exception:
136
+ self.logger.warning("Invalid GOOGLE_DRIVE_CLIENT_SECRET_JSON", exc_info=True)
137
+ if raw_b64:
138
+ try:
139
+ return json.loads(base64.b64decode(raw_b64).decode("utf-8"))
140
+ except Exception:
141
+ self.logger.warning("Invalid GOOGLE_DRIVE_CLIENT_SECRET_JSON_BASE64", exc_info=True)
142
+ if path:
143
+ try:
144
+ return json.loads(Path(path).expanduser().read_text(encoding="utf-8"))
145
+ except Exception:
146
+ self.logger.warning("Failed to load Google Drive client secret path %s", path, exc_info=True)
147
+ client_id = str(os.getenv("GOOGLE_DRIVE_CLIENT_ID", "")).strip()
148
+ client_secret = str(os.getenv("GOOGLE_DRIVE_CLIENT_SECRET", "")).strip()
149
+ token_uri = str(os.getenv("GOOGLE_DRIVE_TOKEN_URI", "https://oauth2.googleapis.com/token")).strip()
150
+ if client_id and client_secret:
151
+ return {
152
+ "installed": {
153
+ "client_id": client_id,
154
+ "client_secret": client_secret,
155
+ "token_uri": token_uri,
156
+ }
157
+ }
158
+ return None
159
+
160
+ def _oauth_client(self) -> dict[str, str]:
161
+ payload = self._client_secret_payload() or {}
162
+ installed = payload.get("installed") or payload.get("web") or {}
163
+ return {
164
+ "client_id": str(installed.get("client_id") or "").strip(),
165
+ "client_secret": str(installed.get("client_secret") or "").strip(),
166
+ "token_uri": str(installed.get("token_uri") or "https://oauth2.googleapis.com/token").strip(),
167
+ }
168
+
169
+ def _refresh_token(self) -> str:
170
+ return str(os.getenv("GOOGLE_DRIVE_REFRESH_TOKEN", "") or self._control_plane_token("google_drive_refresh_token") or "").strip()
171
+
172
+ def _access_token(self) -> str:
173
+ token = str(os.getenv("GOOGLE_DRIVE_ACCESS_TOKEN", "") or self._control_plane_token("google_drive_access_token") or "").strip()
174
+ expires_at = float(str(os.getenv("GOOGLE_DRIVE_TOKEN_EXPIRES_AT", "0") or "0") or 0)
175
+ if token and (not expires_at or expires_at > (time.time() + 60)):
176
+ return token
177
+ refreshed = self._refresh_access_token()
178
+ return refreshed or token
179
+
180
+ def _refresh_access_token(self) -> str:
181
+ refresh_token = self._refresh_token()
182
+ client = self._oauth_client()
183
+ if not refresh_token or not client["client_id"] or not client["client_secret"]:
184
+ return ""
185
+ response = requests.post(
186
+ client["token_uri"],
187
+ data={
188
+ "grant_type": "refresh_token",
189
+ "refresh_token": refresh_token,
190
+ "client_id": client["client_id"],
191
+ "client_secret": client["client_secret"],
192
+ },
193
+ timeout=30,
194
+ )
195
+ response.raise_for_status()
196
+ payload = response.json()
197
+ access_token = str(payload.get("access_token") or "").strip()
198
+ expires_in = float(payload.get("expires_in") or 0)
199
+ if access_token:
200
+ os.environ["GOOGLE_DRIVE_ACCESS_TOKEN"] = access_token
201
+ os.environ["GOOGLE_DRIVE_TOKEN_EXPIRES_AT"] = str(time.time() + expires_in) if expires_in else "0"
202
+ return access_token
203
+
204
+ def _request(self, method: str, url: str, *, headers: dict[str, str] | None = None, timeout: int = 60, **kwargs) -> requests.Response:
205
+ token = self._access_token()
206
+ if not token:
207
+ raise ValueError("Google Drive access token is not configured")
208
+ base_headers = dict(headers or {})
209
+ response: requests.Response | None = None
210
+ for _ in range(2):
211
+ response = requests.request(
212
+ method,
213
+ url,
214
+ headers={**base_headers, "Authorization": f"Bearer {token}"},
215
+ timeout=timeout,
216
+ **kwargs,
217
+ )
218
+ if response.status_code != 401:
219
+ response.raise_for_status()
220
+ return response
221
+ token = self._refresh_access_token()
222
+ if not token:
223
+ break
224
+ assert response is not None
225
+ response.raise_for_status()
226
+ return response
227
+
228
+ @staticmethod
229
+ def _escape_query(value: str) -> str:
230
+ return str(value or "").replace("\\", "\\\\").replace("'", "\\'")
231
+
232
+ def _find_folder(self, name: str, parent_id: str) -> str:
233
+ query = (
234
+ "mimeType = 'application/vnd.google-apps.folder' and trashed = false "
235
+ f"and name = '{self._escape_query(name)}'"
236
+ )
237
+ if parent_id:
238
+ query += f" and '{self._escape_query(parent_id)}' in parents"
239
+ response = self._request(
240
+ "GET",
241
+ "https://www.googleapis.com/drive/v3/files",
242
+ params={"q": query, "fields": "files(id,name)", "pageSize": 10, "supportsAllDrives": "true"},
243
+ )
244
+ files = response.json().get("files", [])
245
+ return str(files[0].get("id") or "").strip() if files else ""
246
+
247
+ def _create_folder(self, name: str, parent_id: str) -> str:
248
+ payload: dict[str, Any] = {"name": name, "mimeType": "application/vnd.google-apps.folder"}
249
+ if parent_id:
250
+ payload["parents"] = [parent_id]
251
+ response = self._request(
252
+ "POST",
253
+ "https://www.googleapis.com/drive/v3/files?supportsAllDrives=true",
254
+ headers={"Content-Type": "application/json; charset=UTF-8"},
255
+ json=payload,
256
+ )
257
+ return str(response.json().get("id") or "").strip()
258
+
259
+ def _ensure_folder_path(self, relative_path: str) -> str:
260
+ current_parent = self.root_folder_id()
261
+ parts = [part for part in f"{self.prefix()}/{relative_path}".replace("\\", "/").split("/") if part.strip()]
262
+ for part in parts:
263
+ safe_part = _safe_name(part, "folder")
264
+ cache_key = f"{current_parent}:{safe_part}"
265
+ cached = self._folder_cache.get(cache_key)
266
+ if cached:
267
+ current_parent = cached
268
+ continue
269
+ folder_id = self._find_folder(safe_part, current_parent)
270
+ if not folder_id:
271
+ folder_id = self._create_folder(safe_part, current_parent)
272
+ self._folder_cache[cache_key] = folder_id
273
+ current_parent = folder_id
274
+ return current_parent
275
+
276
+ def _find_file(self, folder_id: str, file_name: str) -> str:
277
+ cache_key = f"{folder_id}:{file_name}"
278
+ cached = self._file_cache.get(cache_key)
279
+ if cached:
280
+ return cached
281
+ query = (
282
+ "trashed = false "
283
+ f"and name = '{self._escape_query(file_name)}' "
284
+ f"and '{self._escape_query(folder_id)}' in parents"
285
+ )
286
+ response = self._request(
287
+ "GET",
288
+ "https://www.googleapis.com/drive/v3/files",
289
+ params={"q": query, "fields": "files(id,name)", "pageSize": 10, "supportsAllDrives": "true"},
290
+ )
291
+ files = response.json().get("files", [])
292
+ file_id = str(files[0].get("id") or "").strip() if files else ""
293
+ if file_id:
294
+ self._file_cache[cache_key] = file_id
295
+ return file_id
296
+
297
+ def _create_file(self, folder_id: str, file_name: str) -> str:
298
+ response = self._request(
299
+ "POST",
300
+ "https://www.googleapis.com/drive/v3/files?supportsAllDrives=true",
301
+ headers={"Content-Type": "application/json; charset=UTF-8"},
302
+ json={"name": file_name, "parents": [folder_id]},
303
+ )
304
+ file_id = str(response.json().get("id") or "").strip()
305
+ if file_id:
306
+ self._file_cache[f"{folder_id}:{file_name}"] = file_id
307
+ return file_id
308
+
309
+ def _upload_json(self, file_id: str, payload: dict[str, Any]) -> None:
310
+ self._request(
311
+ "PATCH",
312
+ f"https://www.googleapis.com/upload/drive/v3/files/{file_id}?uploadType=media&supportsAllDrives=true",
313
+ headers={"Content-Type": "application/json; charset=UTF-8"},
314
+ data=json.dumps(payload, ensure_ascii=False, sort_keys=True).encode("utf-8"),
315
+ )
316
+
317
+ def _download_json(self, file_id: str) -> dict[str, Any]:
318
+ response = self._request(
319
+ "GET",
320
+ f"https://www.googleapis.com/drive/v3/files/{file_id}?alt=media&supportsAllDrives=true",
321
+ )
322
+ return dict(response.json() or {})
323
+
324
+ def get_document(self, collection: str, doc_id: str) -> dict[str, Any]:
325
+ if not self.enabled():
326
+ return {}
327
+ folder_id = self._ensure_folder_path(_safe_name(collection, "collection"))
328
+ file_name = f"{_safe_name(doc_id)}.json"
329
+ file_id = self._find_file(folder_id, file_name)
330
+ if not file_id:
331
+ return {}
332
+ try:
333
+ payload = self._download_json(file_id)
334
+ payload.setdefault("id", _safe_name(doc_id))
335
+ return payload
336
+ except Exception:
337
+ self.logger.warning("Failed to download shared-state file %s/%s", collection, doc_id, exc_info=True)
338
+ return {}
339
+
340
+ def set_document(self, collection: str, doc_id: str, payload: dict[str, Any], *, merge: bool = True) -> bool:
341
+ if not self.enabled():
342
+ return False
343
+ safe_doc = _safe_name(doc_id)
344
+ try:
345
+ existing = self.get_document(collection, safe_doc) if merge else {}
346
+ combined = {**existing, **dict(payload or {})} if merge else dict(payload or {})
347
+ combined.setdefault("id", safe_doc)
348
+ folder_id = self._ensure_folder_path(_safe_name(collection, "collection"))
349
+ file_name = f"{safe_doc}.json"
350
+ file_id = self._find_file(folder_id, file_name)
351
+ if not file_id:
352
+ file_id = self._create_file(folder_id, file_name)
353
+ self._upload_json(file_id, combined)
354
+ return True
355
+ except Exception:
356
+ self.logger.warning("Failed to upload shared-state file %s/%s", collection, safe_doc, exc_info=True)
357
+ return False
358
+
359
+ def list_documents(self, collection: str, *, limit: int = 200) -> list[dict[str, Any]]:
360
+ if not self.enabled():
361
+ return []
362
+ try:
363
+ folder_id = self._ensure_folder_path(_safe_name(collection, "collection"))
364
+ query = f"trashed = false and '{self._escape_query(folder_id)}' in parents"
365
+ response = self._request(
366
+ "GET",
367
+ "https://www.googleapis.com/drive/v3/files",
368
+ params={
369
+ "q": query,
370
+ "fields": "files(id,name,modifiedTime)",
371
+ "pageSize": max(1, int(limit)),
372
+ "supportsAllDrives": "true",
373
+ "orderBy": "modifiedTime desc",
374
+ },
375
+ )
376
+ items: list[dict[str, Any]] = []
377
+ for file in response.json().get("files", []):
378
+ file_id = str(file.get("id") or "").strip()
379
+ file_name = str(file.get("name") or "").strip()
380
+ if not file_id or not file_name.endswith(".json"):
381
+ continue
382
+ self._file_cache[f"{folder_id}:{file_name}"] = file_id
383
+ payload = self._download_json(file_id)
384
+ payload.setdefault("id", file_name[:-5])
385
+ items.append(payload)
386
+ return items
387
+ except Exception:
388
+ self.logger.warning("Failed to list shared-state collection %s", collection, exc_info=True)
389
+ return []
390
+
391
+ def delete_document(self, collection: str, doc_id: str) -> bool:
392
+ if not self.enabled():
393
+ return False
394
+ safe_doc = _safe_name(doc_id)
395
+ try:
396
+ folder_id = self._ensure_folder_path(_safe_name(collection, "collection"))
397
+ file_name = f"{safe_doc}.json"
398
+ file_id = self._find_file(folder_id, file_name)
399
+ if not file_id:
400
+ return True
401
+ self._request(
402
+ "DELETE",
403
+ f"https://www.googleapis.com/drive/v3/files/{file_id}?supportsAllDrives=true",
404
+ )
405
+ self._file_cache.pop(f"{folder_id}:{file_name}", None)
406
+ return True
407
+ except Exception:
408
+ self.logger.warning("Failed to delete shared-state file %s/%s", collection, safe_doc, exc_info=True)
409
+ return False
shared/remote_env.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Encrypted remote .env loader and publisher helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import io
7
+ import json
8
+ import logging
9
+ import os
10
+ from typing import Any
11
+
12
+ import requests
13
+ from cryptography.fernet import Fernet
14
+ from cryptography.hazmat.primitives import hashes
15
+ from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
16
+ from dotenv import dotenv_values
17
+
18
+
19
+ def _decode_maybe_b64(value: str) -> str:
20
+ text = str(value or "").strip()
21
+ if not text:
22
+ return ""
23
+ try:
24
+ padded = text + "=" * (-len(text) % 4)
25
+ decoded = base64.urlsafe_b64decode(padded.encode("utf-8")).decode("utf-8")
26
+ if decoded:
27
+ return decoded
28
+ except Exception:
29
+ pass
30
+ return text
31
+
32
+
33
+ def _derive_key(password: str, salt: bytes) -> bytes:
34
+ kdf = PBKDF2HMAC(
35
+ algorithm=hashes.SHA256(),
36
+ length=32,
37
+ salt=salt,
38
+ iterations=390000,
39
+ )
40
+ return base64.urlsafe_b64encode(kdf.derive(password.encode("utf-8")))
41
+
42
+
43
+ def build_remote_env_bundle(env_text: str, password: str, metadata: dict[str, Any] | None = None) -> dict[str, Any]:
44
+ salt = os.urandom(16)
45
+ key = _derive_key(password, salt)
46
+ token = Fernet(key).encrypt((env_text or "").encode("utf-8"))
47
+ return {
48
+ "schema": "kapo-remote-env-v1",
49
+ "salt": base64.urlsafe_b64encode(salt).decode("utf-8"),
50
+ "token": token.decode("utf-8"),
51
+ "metadata": dict(metadata or {}),
52
+ }
53
+
54
+
55
+ def decrypt_remote_env_bundle(payload: dict[str, Any], password: str) -> str:
56
+ salt_raw = str(payload.get("salt") or "").strip()
57
+ token = str(payload.get("token") or "").strip()
58
+ if not salt_raw or not token:
59
+ raise ValueError("Remote env payload is incomplete")
60
+ salt = base64.urlsafe_b64decode(salt_raw.encode("utf-8"))
61
+ key = _derive_key(password, salt)
62
+ return Fernet(key).decrypt(token.encode("utf-8")).decode("utf-8")
63
+
64
+
65
+ def _remote_env_url() -> str:
66
+ return _decode_maybe_b64(
67
+ str(os.getenv("KAPO_REMOTE_ENV_URL", "") or os.getenv("KAPO_REMOTE_ENV_URL_B64", "") or "").strip()
68
+ )
69
+
70
+
71
+ def _remote_env_password() -> str:
72
+ return _decode_maybe_b64(
73
+ str(os.getenv("KAPO_REMOTE_ENV_PASSWORD", "") or os.getenv("KAPO_REMOTE_ENV_PASSWORD_B64", "") or "").strip()
74
+ )
75
+
76
+
77
+ def load_remote_env_if_configured(*, override: bool = True, logger_name: str = "kapo.remote_env") -> dict[str, str]:
78
+ logger = logging.getLogger(logger_name)
79
+ if str(os.getenv("KAPO_REMOTE_ENV_LOADED", "")).strip().lower() in {"1", "true", "yes", "on"}:
80
+ return {}
81
+ url = _remote_env_url()
82
+ password = _remote_env_password()
83
+ if not url or not password:
84
+ return {}
85
+ try:
86
+ response = requests.get(url, timeout=30)
87
+ response.raise_for_status()
88
+ payload = dict(response.json() or {})
89
+ env_text = decrypt_remote_env_bundle(payload, password)
90
+ parsed = {
91
+ str(key): str(value)
92
+ for key, value in dotenv_values(stream=io.StringIO(env_text)).items()
93
+ if key and value is not None
94
+ }
95
+ for key, value in parsed.items():
96
+ if override or key not in os.environ or not str(os.getenv(key) or "").strip():
97
+ os.environ[key] = value
98
+ os.environ["KAPO_REMOTE_ENV_LOADED"] = "1"
99
+ return parsed
100
+ except Exception:
101
+ logger.warning("Failed to load encrypted remote env bundle", exc_info=True)
102
+ return {}