Percy3822 commited on
Commit
dd69c3f
·
verified ·
1 Parent(s): fca3931

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +194 -172
app.py CHANGED
@@ -1,190 +1,212 @@
1
  import os
 
 
 
 
 
2
  import httpx
3
- from typing import List, Dict, Any, Optional, Literal
4
- from fastapi import FastAPI, HTTPException, UploadFile, File
5
- from fastapi.responses import FileResponse
6
  from fastapi.staticfiles import StaticFiles
7
- from pydantic import BaseModel
8
-
9
- # =========================
10
- # Config
11
- # =========================
12
- PYTHON_AI_URL = os.getenv("PYTHON_AI_URL", "")
13
- TTS_URL = os.getenv("TTS_URL", "")
14
- STT_URL = os.getenv("STT_URL", "")
15
-
16
- # =========================
17
- # Schemas
18
- # =========================
19
- class Cursor(BaseModel):
20
- l: int
21
- c: int
22
-
23
- class Viewport(BaseModel):
24
- start: int
25
- end: int
26
- text: str
27
-
28
- class Diagnostic(BaseModel):
29
- l: int
30
- sev: str
31
- msg: str
32
-
33
- class Memory(BaseModel):
34
- short: List[str] = []
35
- sess: List[str] = []
36
- proj: List[str] = []
37
-
38
- class Telemetry(BaseModel):
39
- file: str
40
- lang: str
41
- cursor: Cursor
42
- viewport: Viewport
43
- diag: List[Diagnostic] = []
44
- term: str = ""
45
-
46
- class CodeHelpIn(BaseModel):
47
- utterance: str
48
- telemetry: Telemetry
49
- memory: Memory = Memory()
50
- response_mode: Literal["patch","full"] = "patch"
51
-
52
- class PythonAIOutput(BaseModel):
53
- mode: Literal["patch","full","ask"]
54
- patch: str = ""
55
- full_text: str = ""
56
- explanation: str = ""
57
- confidence: float = 0.5
58
-
59
- class CodeHelpOut(BaseModel):
60
- ai: PythonAIOutput
61
- tts_audio_url: Optional[str] = None
62
- notes: Dict[str, Any] = {}
63
-
64
- # =========================
65
- # App + UI
66
- # =========================
67
- app = FastAPI(title="Brain", version="1.0")
68
  app.mount("/static", StaticFiles(directory="static"), name="static")
69
 
70
- @app.get("/")
71
- async def root_ui():
72
- return FileResponse("static/ui.html")
73
-
74
- client: Optional[httpx.AsyncClient] = None
75
-
76
- @app.on_event("startup")
77
- async def startup():
78
- global client
79
- client = httpx.AsyncClient()
80
-
81
- @app.on_event("shutdown")
82
- async def shutdown():
83
- global client
84
- if client:
85
- await client.aclose()
86
-
87
- async def _safe_post_json(url: str, payload: Dict[str, Any]) -> Dict[str, Any]:
88
- if client is None:
89
- raise HTTPException(status_code=500, detail="client not ready")
90
- r = await client.post(url, json=payload)
91
- r.raise_for_status()
92
- return r.json()
93
-
94
- # =========================
95
- # Health & Warmup
96
- # =========================
 
 
 
 
 
97
  @app.get("/health")
98
  async def health():
99
- deps = {
100
- "python_ai": bool(PYTHON_AI_URL),
101
- "tts": bool(TTS_URL),
102
- "stt": bool(STT_URL),
103
- }
104
- return {"ok": True, "deps": deps}
 
 
 
 
 
 
 
 
105
 
106
  @app.post("/warmup")
107
  async def warmup():
108
  notes = {}
109
- if PYTHON_AI_URL:
 
110
  try:
111
- res = await _safe_post_json(f"{PYTHON_AI_URL}/code_help", {
112
- "intent":"ping","file":"warmup.py","lang":"python",
113
- "cursor":{"l":1,"c":1},
114
- "viewport":{"start":1,"end":1,"text":"print('warmup')"},
115
- "diag": [], "term":"", "mem":{"short":[],"sess":[],"proj":[]}
116
- })
117
- notes["python_ai"] = "ok" if res else "fail"
 
 
 
 
 
 
 
 
118
  except Exception as e:
119
  notes["python_ai"] = f"err: {e}"
120
- if TTS_URL:
 
121
  try:
122
- res = await _safe_post_json(f"{TTS_URL}/speak", {"text":"warmup"})
123
- notes["tts"] = "ok" if "audio_path" in res else "fail"
 
 
 
124
  except Exception as e:
125
  notes["tts"] = f"err: {e}"
126
- if STT_URL:
127
- notes["stt"] = "configured"
128
- return {"ok": True, "notes": notes}
129
-
130
- # =========================
131
- # Code Help
132
- # =========================
133
- @app.post("/code_help", response_model=CodeHelpOut)
134
- async def code_help(x: CodeHelpIn):
135
- if not PYTHON_AI_URL:
136
- raise HTTPException(status_code=500, detail="PYTHON_AI_URL not configured")
137
- res = await _safe_post_json(f"{PYTHON_AI_URL}/code_help", {
138
- "intent": x.utterance,
139
- "file": x.telemetry.file,
140
- "lang": x.telemetry.lang,
141
- "cursor": {"l": x.telemetry.cursor.l, "c": x.telemetry.cursor.c},
142
- "viewport": {"start": x.telemetry.viewport.start, "end": x.telemetry.viewport.end, "text": x.telemetry.viewport.text},
143
- "diag": [{"l": d.l, "sev": d.sev, "msg": d.msg} for d in x.telemetry.diag],
144
- "term": x.telemetry.term,
145
- "mem": {"short": x.memory.short, "sess": x.memory.sess, "proj": x.memory.proj}
146
- })
147
- ai_out = PythonAIOutput(**res)
148
- tts_url = None
149
- if TTS_URL:
150
  try:
151
- r = await _safe_post_json(f"{TTS_URL}/speak", {"text": ai_out.explanation})
152
- if "audio_path" in r:
153
- base = TTS_URL.rstrip("/")
154
- name = r["audio_path"].split("/")[-1]
155
- tts_url = f"{base}/file/{name}"
156
- except:
157
- pass
158
- return CodeHelpOut(ai=ai_out, tts_audio_url=tts_url, notes={})
159
-
160
- # =========================
161
- # STT Proxy
162
- # =========================
163
- @app.post("/stt_transcribe")
164
- async def stt_transcribe(file: UploadFile = File(...)):
165
- if not STT_URL:
166
- raise HTTPException(status_code=500, detail="STT_URL not configured")
167
- if client is None:
168
- raise HTTPException(status_code=500, detail="client not ready")
169
- data = await file.read()
170
- files = {"file": (file.filename or "audio.wav", data, file.content_type or "audio/wav")}
171
- r = await client.post(f"{STT_URL}/transcribe", files=files)
172
- r.raise_for_status()
173
- return r.json()
174
-
175
- # =========================
176
- # TTS Proxy
177
- # =========================
178
- class SpeakIn(BaseModel):
179
- text: str
180
-
181
- @app.post("/tts_speak")
182
- async def tts_speak(x: SpeakIn):
183
- if not TTS_URL:
184
- raise HTTPException(status_code=500, detail="TTS_URL not configured")
185
- r = await _safe_post_json(f"{TTS_URL}/speak", {"text": x.text})
186
- if "audio_path" not in r:
187
- raise HTTPException(status_code=502, detail="TTS failed")
188
- base = TTS_URL.rstrip("/")
189
- name = r["audio_path"].split("/")[-1]
190
- return {"tts_audio_url": f"{base}/file/{name}"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
2
+ import json
3
+ import uuid
4
+ import asyncio
5
+ from typing import Dict, Any
6
+
7
  import httpx
8
+ from fastapi import FastAPI, UploadFile, File, HTTPException, Request
9
+ from fastapi.responses import JSONResponse, HTMLResponse
 
10
  from fastapi.staticfiles import StaticFiles
11
+
12
+ APP_TITLE = "Brain (Orchestrator)"
13
+ PYTHON_AI_URL = os.getenv("PYTHON_AI_URL", "").strip()
14
+ TTS_URL = os.getenv("TTS_URL", "").strip()
15
+ STT_URL = os.getenv("STT_URL", "").strip()
16
+ REQUEST_TIMEOUT = float(os.getenv("REQUEST_TIMEOUT", "25"))
17
+
18
+ DATA_DIR = "/data"
19
+ MEM_PATH = os.path.join(DATA_DIR, "memory.json")
20
+
21
+ app = FastAPI(title=APP_TITLE)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  app.mount("/static", StaticFiles(directory="static"), name="static")
23
 
24
+ # ---------- Memory (persisted to /data/memory.json) ----------
25
+ def _ensure_memory_file():
26
+ os.makedirs(DATA_DIR, exist_ok=True)
27
+ if not os.path.exists(MEM_PATH):
28
+ with open(MEM_PATH, "w", encoding="utf-8") as f:
29
+ json.dump({"short": [], "sess": [], "proj": []}, f)
30
+
31
+ def _read_mem() -> Dict[str, Any]:
32
+ _ensure_memory_file()
33
+ with open(MEM_PATH, "r", encoding="utf-8") as f:
34
+ return json.load(f)
35
+
36
+ def _write_mem(m: Dict[str, Any]):
37
+ with open(MEM_PATH, "w", encoding="utf-8") as f:
38
+ json.dump(m, f, ensure_ascii=False, indent=2)
39
+
40
+ # ---------- Helpers ----------
41
+ def ok(data: Dict[str, Any] = None):
42
+ return JSONResponse({"ok": True, **(data or {})})
43
+
44
+ def err(where: str, message: str, status: int = 502):
45
+ return JSONResponse({"ok": False, "error": message, "where": where}, status_code=status)
46
+
47
+ def _absolute_audio_url(path: str) -> str:
48
+ if not path:
49
+ return ""
50
+ if path.startswith("http://") or path.startswith("https://"):
51
+ return path
52
+ # TTS spaces usually serve files at /file/<name>
53
+ return f"{TTS_URL.rstrip('/')}/file/{path.lstrip('/').replace('file/', '')}"
54
+
55
+ # ---------- Routes ----------
56
  @app.get("/health")
57
  async def health():
58
+ return ok({
59
+ "service": "brain",
60
+ "version": "v2",
61
+ "deps": {
62
+ "python_ai": PYTHON_AI_URL,
63
+ "tts": TTS_URL,
64
+ "stt": STT_URL
65
+ }
66
+ })
67
+
68
+ @app.get("/ui")
69
+ async def ui():
70
+ with open("static/ui.html", "r", encoding="utf-8") as f:
71
+ return HTMLResponse(f.read())
72
 
73
  @app.post("/warmup")
74
  async def warmup():
75
  notes = {}
76
+ async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT, follow_redirects=True) as client:
77
+ # Python AI
78
  try:
79
+ # prefer /health if exists; otherwise /code_help "ping"
80
+ try:
81
+ r = await client.get(f"{PYTHON_AI_URL}/health")
82
+ notes["python_ai"] = "ok" if r.status_code == 200 else f"status {r.status_code}"
83
+ except Exception:
84
+ payload = {
85
+ "utterance": "ping",
86
+ "telemetry": {"file":"x.py","lang":"python","cursor":{"l":1,"c":1},
87
+ "viewport":{"start":1,"end":1,"text":"print(1)\n"},
88
+ "diag":[],"term":""},
89
+ "memory":{"short":[],"sess":[],"proj":[]},
90
+ "response_mode":"patch"
91
+ }
92
+ r = await client.post(f"{PYTHON_AI_URL}/code_help", json=payload)
93
+ notes["python_ai"] = "ok" if r.status_code == 200 else f"status {r.status_code}"
94
  except Exception as e:
95
  notes["python_ai"] = f"err: {e}"
96
+
97
+ # TTS
98
  try:
99
+ r = await client.post(f"{TTS_URL}/speak", json={"text":"ping"})
100
+ if r.status_code == 200:
101
+ notes["tts"] = "ok"
102
+ else:
103
+ notes["tts"] = f"status {r.status_code}: {r.text[:120]}"
104
  except Exception as e:
105
  notes["tts"] = f"err: {e}"
106
+
107
+ # STT – some spaces lack /health, so check openapi.json
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  try:
109
+ r = await client.get(f"{STT_URL}/openapi.json")
110
+ notes["stt"] = "ok" if r.status_code == 200 else f"status {r.status_code}"
111
+ except Exception as e:
112
+ notes["stt"] = f"err: {e}"
113
+
114
+ return ok({"notes": notes})
115
+
116
+ # ---- Proxies to features ----
117
+ @app.post("/speak")
118
+ async def speak(request: Request):
119
+ body = await request.json()
120
+ text = (body.get("text") or "").strip()
121
+ if not text:
122
+ raise HTTPException(400, "text is required")
123
+ async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT, follow_redirects=True) as client:
124
+ try:
125
+ r = await client.post(f"{TTS_URL}/speak", json={"text": text})
126
+ if r.status_code != 200:
127
+ return err("tts.speak", f"{r.status_code}: {r.text}")
128
+ data = r.json()
129
+ # normalize to audio_url
130
+ audio_url = data.get("audio_url") or _absolute_audio_url(data.get("audio_path",""))
131
+ return ok({"audio_url": audio_url})
132
+ except Exception as e:
133
+ return err("tts.speak", str(e))
134
+
135
+ @app.post("/transcribe")
136
+ async def transcribe(file: UploadFile = File(...)):
137
+ # forward multipart to STT
138
+ async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT, follow_redirects=True) as client:
139
+ try:
140
+ files = {"file": (file.filename, await file.read(), file.content_type or "audio/wav")}
141
+ r = await client.post(f"{STT_URL}/transcribe", files=files)
142
+ if r.status_code != 200:
143
+ return err("stt.transcribe", f"{r.status_code}: {r.text}")
144
+ return ok({"stt": r.json()})
145
+ except Exception as e:
146
+ return err("stt.transcribe", str(e))
147
+
148
+ @app.post("/code_help")
149
+ async def code_help(request: Request):
150
+ payload = await request.json()
151
+ async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT, follow_redirects=True) as client:
152
+ try:
153
+ r = await client.post(f"{PYTHON_AI_URL}/code_help", json=payload)
154
+ if r.status_code == 200:
155
+ return ok({"ai": r.json()})
156
+ # fallback: tiny patch if upstream rejects
157
+ vp = (((payload or {}).get("telemetry") or {}).get("viewport") or {}).get("text","")
158
+ if "reslt" in vp:
159
+ patch = {
160
+ "mode": "patch",
161
+ "patch": "--- a/main.py\n+++ b/main.py\n@@\n-print(reslt)\n+print(result)\n",
162
+ "full_text": "",
163
+ "explanation": "Fixed misspelling: reslt -> result.",
164
+ "confidence": 0.6,
165
+ "need": {"function": False, "xrefs": [], "page_ids": []}
166
+ }
167
+ return ok({"ai": patch, "note":"fallback"})
168
+ return err("python_ai.code_help", f"{r.status_code}: {r.text}")
169
+ except Exception as e:
170
+ # offline fallback
171
+ return ok({"ai":{
172
+ "mode":"patch",
173
+ "patch":"--- a/x.py\n+++ b/x.py\n@@\n-print(1)\n+print(1)\n",
174
+ "full_text": "",
175
+ "explanation":"Fallback (upstream unreachable).",
176
+ "confidence":0.3,
177
+ "need":{"function":False,"xrefs":[],"page_ids":[]}
178
+ }, "note": f"fallback: {e}"})
179
+
180
+ # ---- Memory API ----
181
+ @app.get("/memory")
182
+ async def memory_get():
183
+ return ok({"memory": _read_mem()})
184
+
185
+ @app.post("/memory/save")
186
+ async def memory_save(request: Request):
187
+ body = await request.json()
188
+ bucket = (body.get("bucket") or "").strip()
189
+ item = (body.get("item") or "").strip()
190
+ if bucket not in ("short", "sess", "proj"):
191
+ raise HTTPException(400, "bucket must be one of: short | sess | proj")
192
+ if not item:
193
+ raise HTTPException(400, "item is required")
194
+ m = _read_mem()
195
+ m.setdefault(bucket, [])
196
+ m[bucket].append(item)
197
+ _write_mem(m)
198
+ return ok({"memory": m})
199
+
200
+ @app.post("/memory/clear")
201
+ async def memory_clear(request: Request):
202
+ body = await request.json()
203
+ bucket = (body.get("bucket") or "").strip()
204
+ if not bucket:
205
+ m = {"short": [], "sess": [], "proj": []}
206
+ else:
207
+ if bucket not in ("short", "sess", "proj"):
208
+ raise HTTPException(400, "bucket must be one of: short | sess | proj")
209
+ m = _read_mem()
210
+ m[bucket] = []
211
+ _write_mem(m)
212
+ return ok({"memory": m})