Percy3822 commited on
Commit
d52b394
·
verified ·
1 Parent(s): 53c9e29

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +82 -126
app.py CHANGED
@@ -3,7 +3,7 @@ import asyncio
3
  from typing import List, Dict, Any, Optional, Literal
4
 
5
  import httpx
6
- from fastapi import FastAPI, HTTPException
7
  from fastapi.responses import FileResponse
8
  from fastapi.staticfiles import StaticFiles
9
  from pydantic import BaseModel, Field, ValidationError
@@ -28,8 +28,8 @@ CONNECT_TIMEOUT_S = float(os.getenv("CONNECT_TIMEOUT_S", "10"))
28
  # Schemas
29
  # =========================
30
  class Cursor(BaseModel):
31
- l: int = Field(..., description="line")
32
- c: int = Field(..., description="col")
33
 
34
  class Viewport(BaseModel):
35
  start: int
@@ -81,11 +81,9 @@ class CodeHelpOut(BaseModel):
81
  notes: Dict[str, Any] = {}
82
 
83
  # =========================
84
- # App + Static UI
85
  # =========================
86
- app = FastAPI(title="Brain (Router)", version="1.0")
87
-
88
- # Serve the static UI
89
  app.mount("/static", StaticFiles(directory="static"), name="static")
90
 
91
  @app.get("/")
@@ -93,7 +91,7 @@ async def root_ui():
93
  return FileResponse("static/ui.html")
94
 
95
  # =========================
96
- # HTTP client (async)
97
  # =========================
98
  client: Optional[httpx.AsyncClient] = None
99
 
@@ -102,64 +100,21 @@ async def _startup():
102
  global client
103
  client = httpx.AsyncClient(
104
  timeout=httpx.Timeout(REQUEST_TIMEOUT_S, connect=CONNECT_TIMEOUT_S),
105
- headers={"User-Agent": "BrainRouter/1.0"}
106
  )
107
- asyncio.create_task(worker_loop())
108
- asyncio.create_task(worker_loop())
109
 
110
  @app.on_event("shutdown")
111
  async def _shutdown():
112
  global client
113
- try:
114
- if client is not None:
115
- await client.aclose()
116
- except Exception:
117
- pass
118
-
119
- # =========================
120
- # Small Utilities
121
- # =========================
122
- def _truncate_bytes(s: str, budget: int) -> str:
123
- b = s.encode("utf-8")
124
- if len(b) <= budget:
125
- return s
126
- return b[:budget].decode("utf-8", errors="ignore")
127
-
128
- def _shrink_lines_to_max(window: Viewport, max_lines: int) -> Viewport:
129
- lines = window.text.splitlines()
130
- if len(lines) <= max_lines:
131
- return window
132
- keep = max_lines
133
- slice_start = max(0, len(lines) - keep)
134
- new_text = "\n".join(lines[slice_start:])
135
- return Viewport(start=window.end - keep + 1, end=window.end, text=new_text)
136
 
137
  async def _safe_post_json(url: str, payload: Dict[str, Any]) -> Dict[str, Any]:
138
  if client is None:
139
- raise HTTPException(status_code=500, detail="HTTP client not initialized")
140
- try:
141
- r = await client.post(url, json=payload)
142
- r.raise_for_status()
143
- return r.json()
144
- except Exception as e:
145
- raise HTTPException(status_code=502, detail=f"POST {url} failed: {e}")
146
-
147
- # =========================
148
- # Priority Queue (reserved)
149
- # =========================
150
- TASK_Q: "asyncio.PriorityQueue[tuple[int,float,dict]]" = asyncio.PriorityQueue()
151
-
152
- async def worker_loop():
153
- while True:
154
- priority, ts, task = await TASK_Q.get()
155
- try:
156
- handler = task.get("handler")
157
- if handler:
158
- await handler(**task.get("args", {}))
159
- except Exception:
160
- pass
161
- finally:
162
- TASK_Q.task_done()
163
 
164
  # =========================
165
  # Health & Warmup
@@ -173,7 +128,7 @@ async def health():
173
  "vision": bool(VISION_URL),
174
  "memory": bool(MEMORY_URL),
175
  }
176
- return {"ok": True, "deps": deps, "version": "1.0"}
177
 
178
  @app.post("/warmup")
179
  async def warmup():
@@ -183,83 +138,84 @@ async def warmup():
183
  res = await _safe_post_json(f"{PYTHON_AI_URL}/code_help", {
184
  "intent":"ping","file":"_warmup_.py","lang":"python",
185
  "cursor":{"l":1,"c":1},
186
- "viewport":{"start":1,"end":1,"text":"print('warmup')\n"},
187
  "diag": [], "term":"", "mem":{"short":[],"sess":[],"proj":[]}
188
  })
189
- notes["python_ai"] = "ok" if res else "no-response"
190
- except HTTPException as e:
191
- notes["python_ai"] = f"err: {e.detail}"
192
  if TTS_URL:
193
  try:
194
- res = await _safe_post_json(f"{TTS_URL}/speak", {"text":"warming up"})
195
- notes["tts"] = "ok" if "audio_path" in res else "no-audio"
196
- except HTTPException as e:
197
- notes["tts"] = f"err: {e.detail}"
 
 
198
  return {"ok": True, "notes": notes}
199
 
200
  # =========================
201
- # Core: Code Help endpoint
202
  # =========================
203
- def _enforce_budgets(t: Telemetry, m: Memory) -> tuple[Telemetry, Memory, int, int]:
204
- t2 = Telemetry(
205
- file=t.file, lang=t.lang, cursor=t.cursor,
206
- viewport=_shrink_lines_to_max(t.viewport, VIEWPORT_MAX_LINES),
207
- diag=t.diag[:5],
208
- term=t.term
209
- )
210
- mem_text = " | ".join(m.short + m.sess + m.proj)
211
- mem_text = _truncate_bytes(mem_text, MEMORY_BUDGET_BYTES)
212
- m2 = Memory(short=[], sess=[mem_text] if mem_text else [], proj=[])
213
- used_mem = len(mem_text.encode("utf-8"))
214
- prompt_bytes = (
215
- len(t2.file) + len(t2.lang) +
216
- len(t2.viewport.text) + sum(len(d.msg) for d in t2.diag) +
217
- len(t2.term) + used_mem
218
- )
219
- return t2, m2, used_mem, prompt_bytes
220
-
221
- async def _route_python_ai(payload: Dict[str, Any]) -> PythonAIOutput:
222
- if not PYTHON_AI_URL:
223
- raise HTTPException(status_code=500, detail="PYTHON_AI_URL not configured")
224
- res = await _safe_post_json(f"{PYTHON_AI_URL}/code_help", payload)
225
- try:
226
- return PythonAIOutput(**res)
227
- except ValidationError as ve:
228
- raise HTTPException(status_code=502, detail=f"Bad AI JSON schema: {ve}")
229
-
230
- async def _send_tts(text: str) -> Optional[str]:
231
- if not TTS_URL or not text:
232
- return None
233
- try:
234
- res = await _safe_post_json(f"{TTS_URL}/speak", {"text": text})
235
- audio_path = res.get("audio_path")
236
- if not audio_path:
237
- return None
238
- base = TTS_URL.rstrip("/")
239
- name = audio_path.split("/")[-1]
240
- return f"{base}/file/{name}"
241
- except HTTPException:
242
- return None
243
-
244
  @app.post("/code_help", response_model=CodeHelpOut)
245
  async def code_help(x: CodeHelpIn):
246
- t2, m2, used_mem, used_prompt = _enforce_budgets(x.telemetry, x.memory)
247
  py_in = {
248
  "intent": x.utterance,
249
- "file": t2.file,
250
- "lang": t2.lang,
251
- "cursor": {"l": t2.cursor.l, "c": t2.cursor.c},
252
- "viewport": {"start": t2.viewport.start, "end": t2.viewport.end, "text": t2.viewport.text},
253
- "diag": [{"l": d.l, "sev": d.sev, "msg": d.msg} for d in t2.diag],
254
- "term": t2.term,
255
- "mem": {"short": m2.short, "sess": m2.sess, "proj": m2.proj}
256
  }
257
- ai_out = await _route_python_ai(py_in)
258
- tts_url = await _send_tts(ai_out.explanation)
259
- return CodeHelpOut(
260
- ai=ai_out,
261
- tts_audio_url=tts_url,
262
- used_memory_bytes=used_mem,
263
- used_prompt_bytes=used_prompt,
264
- notes={"response_mode": x.response_mode}
265
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  from typing import List, Dict, Any, Optional, Literal
4
 
5
  import httpx
6
+ from fastapi import FastAPI, HTTPException, UploadFile, File
7
  from fastapi.responses import FileResponse
8
  from fastapi.staticfiles import StaticFiles
9
  from pydantic import BaseModel, Field, ValidationError
 
28
  # Schemas
29
  # =========================
30
  class Cursor(BaseModel):
31
+ l: int
32
+ c: int
33
 
34
  class Viewport(BaseModel):
35
  start: int
 
81
  notes: Dict[str, Any] = {}
82
 
83
  # =========================
84
+ # App + UI
85
  # =========================
86
+ app = FastAPI(title="Brain (Router)", version="1.2")
 
 
87
  app.mount("/static", StaticFiles(directory="static"), name="static")
88
 
89
  @app.get("/")
 
91
  return FileResponse("static/ui.html")
92
 
93
  # =========================
94
+ # HTTP client
95
  # =========================
96
  client: Optional[httpx.AsyncClient] = None
97
 
 
100
  global client
101
  client = httpx.AsyncClient(
102
  timeout=httpx.Timeout(REQUEST_TIMEOUT_S, connect=CONNECT_TIMEOUT_S),
103
+ headers={"User-Agent": "BrainRouter/1.2"}
104
  )
 
 
105
 
106
  @app.on_event("shutdown")
107
  async def _shutdown():
108
  global client
109
+ if client:
110
+ await client.aclose()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
  async def _safe_post_json(url: str, payload: Dict[str, Any]) -> Dict[str, Any]:
113
  if client is None:
114
+ raise HTTPException(status_code=500, detail="HTTP client not ready")
115
+ r = await client.post(url, json=payload)
116
+ r.raise_for_status()
117
+ return r.json()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
  # =========================
120
  # Health & Warmup
 
128
  "vision": bool(VISION_URL),
129
  "memory": bool(MEMORY_URL),
130
  }
131
+ return {"ok": True, "deps": deps, "version": "1.2"}
132
 
133
  @app.post("/warmup")
134
  async def warmup():
 
138
  res = await _safe_post_json(f"{PYTHON_AI_URL}/code_help", {
139
  "intent":"ping","file":"_warmup_.py","lang":"python",
140
  "cursor":{"l":1,"c":1},
141
+ "viewport":{"start":1,"end":1,"text":"print('warmup')"},
142
  "diag": [], "term":"", "mem":{"short":[],"sess":[],"proj":[]}
143
  })
144
+ notes["python_ai"] = "ok" if res else "fail"
145
+ except Exception as e:
146
+ notes["python_ai"] = f"err: {e}"
147
  if TTS_URL:
148
  try:
149
+ res = await _safe_post_json(f"{TTS_URL}/speak", {"text":"warmup"})
150
+ notes["tts"] = "ok" if "audio_path" in res else "fail"
151
+ except Exception as e:
152
+ notes["tts"] = f"err: {e}"
153
+ if STT_URL:
154
+ notes["stt"] = "configured"
155
  return {"ok": True, "notes": notes}
156
 
157
  # =========================
158
+ # Code Help
159
  # =========================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  @app.post("/code_help", response_model=CodeHelpOut)
161
  async def code_help(x: CodeHelpIn):
 
162
  py_in = {
163
  "intent": x.utterance,
164
+ "file": x.telemetry.file,
165
+ "lang": x.telemetry.lang,
166
+ "cursor": {"l": x.telemetry.cursor.l, "c": x.telemetry.cursor.c},
167
+ "viewport": {"start": x.telemetry.viewport.start, "end": x.telemetry.viewport.end, "text": x.telemetry.viewport.text},
168
+ "diag": [{"l": d.l, "sev": d.sev, "msg": d.msg} for d in x.telemetry.diag],
169
+ "term": x.telemetry.term,
170
+ "mem": {"short": x.memory.short, "sess": x.memory.sess, "proj": x.memory.proj}
171
  }
172
+ res = await _safe_post_json(f"{PYTHON_AI_URL}/code_help", py_in)
173
+ ai_out = PythonAIOutput(**res)
174
+ tts_url = None
175
+ if TTS_URL:
176
+ try:
177
+ r = await _safe_post_json(f"{TTS_URL}/speak", {"text": ai_out.explanation})
178
+ if "audio_path" in r:
179
+ base = TTS_URL.rstrip("/")
180
+ name = r["audio_path"].split("/")[-1]
181
+ tts_url = f"{base}/file/{name}"
182
+ except:
183
+ pass
184
+ return CodeHelpOut(ai=ai_out, tts_audio_url=tts_url, used_memory_bytes=0, used_prompt_bytes=0, notes={})
185
+
186
+ # =========================
187
+ # TTS Proxy
188
+ # =========================
189
+ class SpeakIn(BaseModel):
190
+ text: str
191
+ sample_rate: Optional[int] = None
192
+ length_scale: Optional[float] = None
193
+ noise_scale: Optional[float] = None
194
+ noise_w: Optional[float] = None
195
+
196
+ @app.post("/tts_speak")
197
+ async def tts_speak(x: SpeakIn):
198
+ if not TTS_URL:
199
+ raise HTTPException(status_code=500, detail="TTS_URL not configured")
200
+ payload = {k:v for k,v in x.model_dump().items() if v is not None}
201
+ r = await _safe_post_json(f"{TTS_URL}/speak", payload)
202
+ if "audio_path" not in r:
203
+ raise HTTPException(status_code=502, detail="TTS failed")
204
+ base = TTS_URL.rstrip("/")
205
+ name = r["audio_path"].split("/")[-1]
206
+ return {"tts_audio_url": f"{base}/file/{name}"}
207
+
208
+ # =========================
209
+ # STT Proxy
210
+ # =========================
211
+ @app.post("/stt_transcribe")
212
+ async def stt_transcribe(file: UploadFile = File(...)):
213
+ if not STT_URL:
214
+ raise HTTPException(status_code=500, detail="STT_URL not configured")
215
+ if client is None:
216
+ raise HTTPException(status_code=500, detail="Client not ready")
217
+ data = await file.read()
218
+ files = {"file": (file.filename or "audio.wav", data, file.content_type or "audio/wav")}
219
+ r = await client.post(f"{STT_URL}/transcribe", files=files)
220
+ r.raise_for_status()
221
+ return r.json()