Spaces:
Running on Zero
Running on Zero
fix: stream deployed advisor turns through app api
Browse files- .gitattributes +0 -44
- app.py +88 -51
- static/app.js +44 -20
- tests/test_app.py +40 -0
- tests/test_frontend_copy.py +3 -0
.gitattributes
CHANGED
|
@@ -1,46 +1,2 @@
|
|
| 1 |
# Auto detect text files and perform LF normalization
|
| 2 |
* text=auto
|
| 3 |
-
.venv/lib/python3.11/site-packages/PIL/.dylibs/libavif.16.4.1.dylib filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
.venv/lib/python3.11/site-packages/PIL/.dylibs/libbrotlicommon.1.2.0.dylib filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
.venv/lib/python3.11/site-packages/PIL/.dylibs/libbrotlidec.1.2.0.dylib filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
.venv/lib/python3.11/site-packages/PIL/.dylibs/libfreetype.6.dylib filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
.venv/lib/python3.11/site-packages/PIL/.dylibs/libharfbuzz.0.dylib filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
.venv/lib/python3.11/site-packages/PIL/.dylibs/libjpeg.62.4.0.dylib filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
.venv/lib/python3.11/site-packages/PIL/.dylibs/liblcms2.2.dylib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
.venv/lib/python3.11/site-packages/PIL/.dylibs/liblzma.5.dylib filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
.venv/lib/python3.11/site-packages/PIL/.dylibs/libopenjp2.2.5.4.dylib filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
.venv/lib/python3.11/site-packages/PIL/.dylibs/libpng16.16.dylib filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
.venv/lib/python3.11/site-packages/PIL/.dylibs/libtiff.6.dylib filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
.venv/lib/python3.11/site-packages/PIL/.dylibs/libwebp.7.dylib filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
.venv/lib/python3.11/site-packages/PIL/.dylibs/libwebpmux.3.dylib filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
.venv/lib/python3.11/site-packages/PIL/.dylibs/libxcb.1.1.0.dylib filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
.venv/lib/python3.11/site-packages/PIL/.dylibs/libz.1.3.1.zlib-ng.dylib filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
.venv/lib/python3.11/site-packages/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
.venv/lib/python3.11/site-packages/gradio/media_assets/audio/cantina.wav filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
.venv/lib/python3.11/site-packages/gradio/media_assets/audio/recording1.wav filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
.venv/lib/python3.11/site-packages/gradio/media_assets/audio/sax.wav filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
.venv/lib/python3.11/site-packages/gradio/media_assets/images/groot.jpeg filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
.venv/lib/python3.11/site-packages/gradio/media_assets/images/tower.jpg filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
.venv/lib/python3.11/site-packages/gradio/media_assets/models3d/Duck.glb filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
.venv/lib/python3.11/site-packages/gradio/media_assets/models3d/sofia.stl filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
.venv/lib/python3.11/site-packages/gradio/media_assets/videos/a.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
.venv/lib/python3.11/site-packages/gradio/media_assets/videos/b.avi filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
.venv/lib/python3.11/site-packages/gradio/media_assets/videos/b.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
.venv/lib/python3.11/site-packages/gradio/media_assets/videos/world.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
.venv/lib/python3.11/site-packages/gradio/templates/frontend/static/fonts/Source[[:space:]]Sans[[:space:]]Pro/SourceSansPro-Bold.woff2 filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
.venv/lib/python3.11/site-packages/gradio/templates/frontend/static/fonts/Source[[:space:]]Sans[[:space:]]Pro/SourceSansPro-Regular.woff2 filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
.venv/lib/python3.11/site-packages/gradio/templates/node/build/client/_app/immutable/chunks/5J0xJHOV.js.br filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
.venv/lib/python3.11/site-packages/gradio/templates/node/build/client/_app/immutable/chunks/BMdH9xbf.js.br filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
.venv/lib/python3.11/site-packages/gradio/templates/node/build/client/_app/immutable/chunks/BmzEVe0D.js.br filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
.venv/lib/python3.11/site-packages/gradio/templates/node/build/client/_app/immutable/chunks/CGkloIng.js.br filter=lfs diff=lfs merge=lfs -text
|
| 36 |
-
.venv/lib/python3.11/site-packages/gradio/templates/node/build/client/_app/immutable/chunks/CLj6IfqE.js.br filter=lfs diff=lfs merge=lfs -text
|
| 37 |
-
.venv/lib/python3.11/site-packages/gradio/templates/node/build/client/_app/immutable/chunks/CZccx8Oi.js.br filter=lfs diff=lfs merge=lfs -text
|
| 38 |
-
.venv/lib/python3.11/site-packages/gradio/templates/node/build/client/_app/immutable/chunks/CgysOIik.js.br filter=lfs diff=lfs merge=lfs -text
|
| 39 |
-
.venv/lib/python3.11/site-packages/gradio/templates/node/build/client/_app/immutable/chunks/DuZ9aF7D.js.br filter=lfs diff=lfs merge=lfs -text
|
| 40 |
-
.venv/lib/python3.11/site-packages/gradio/templates/node/build/client/_app/immutable/chunks/Dufjs8Ut.js.br filter=lfs diff=lfs merge=lfs -text
|
| 41 |
-
.venv/lib/python3.11/site-packages/gradio/templates/node/build/client/_app/immutable/chunks/Ku4z6Hhd.js.br filter=lfs diff=lfs merge=lfs -text
|
| 42 |
-
.venv/lib/python3.11/site-packages/pip/_vendor/distlib/t64-arm.exe filter=lfs diff=lfs merge=lfs -text
|
| 43 |
-
.venv/lib/python3.11/site-packages/pip/_vendor/distlib/t64.exe filter=lfs diff=lfs merge=lfs -text
|
| 44 |
-
.venv/lib/python3.11/site-packages/pip/_vendor/distlib/w64-arm.exe filter=lfs diff=lfs merge=lfs -text
|
| 45 |
-
.venv/lib/python3.11/site-packages/pip/_vendor/distlib/w64.exe filter=lfs diff=lfs merge=lfs -text
|
| 46 |
-
.venv/lib/python3.11/site-packages/playwright/driver/node filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 1 |
# Auto detect text files and perform LF normalization
|
| 2 |
* text=auto
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.py
CHANGED
|
@@ -6,7 +6,7 @@ from pathlib import Path
|
|
| 6 |
from typing import Any, Iterator
|
| 7 |
|
| 8 |
from fastapi import Body
|
| 9 |
-
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, Response
|
| 10 |
from gradio import Server
|
| 11 |
|
| 12 |
from hackathon_advisor.agent import AdvisorEngine
|
|
@@ -46,6 +46,48 @@ def _engine_turn(message: str, session: dict[str, Any]):
|
|
| 46 |
return engine.turn(message, session)
|
| 47 |
|
| 48 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
@app.get("/", response_class=HTMLResponse)
|
| 50 |
def home() -> FileResponse:
|
| 51 |
return FileResponse(STATIC_DIR / "index.html")
|
|
@@ -135,6 +177,45 @@ def artifact_png(artifact: dict[str, Any] | None = Body(default=None)) -> Respon
|
|
| 135 |
)
|
| 136 |
|
| 137 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
@app.get("/api/lora-training-kit.zip")
|
| 139 |
def lora_training_kit() -> Response:
|
| 140 |
runtime_status = engine.runtime_status()
|
|
@@ -160,19 +241,13 @@ def tool_contract_check(model_output: str, fallback_query: str = "") -> dict:
|
|
| 160 |
|
| 161 |
@app.api(name="trace_artifact", concurrency_limit=8)
|
| 162 |
def trace_artifact(session_json: str = "{}") -> str:
|
| 163 |
-
|
| 164 |
-
session = json.loads(session_json or "{}")
|
| 165 |
-
except json.JSONDecodeError:
|
| 166 |
-
session = {}
|
| 167 |
return build_trace_jsonl(session, trace_metadata(index))
|
| 168 |
|
| 169 |
|
| 170 |
@app.api(name="field_notes", concurrency_limit=8)
|
| 171 |
def field_notes_artifact(session_json: str = "{}") -> str:
|
| 172 |
-
|
| 173 |
-
session = json.loads(session_json or "{}")
|
| 174 |
-
except json.JSONDecodeError:
|
| 175 |
-
session = {}
|
| 176 |
return build_field_notes_markdown(
|
| 177 |
session,
|
| 178 |
{
|
|
@@ -184,10 +259,7 @@ def field_notes_artifact(session_json: str = "{}") -> str:
|
|
| 184 |
|
| 185 |
@app.api(name="chapter", concurrency_limit=8)
|
| 186 |
def chapter_artifact(session_json: str = "{}") -> str:
|
| 187 |
-
|
| 188 |
-
session = json.loads(session_json or "{}")
|
| 189 |
-
except json.JSONDecodeError:
|
| 190 |
-
session = {}
|
| 191 |
return build_chapter_markdown(
|
| 192 |
session,
|
| 193 |
{
|
|
@@ -199,10 +271,7 @@ def chapter_artifact(session_json: str = "{}") -> str:
|
|
| 199 |
|
| 200 |
@app.api(name="lora_dataset", concurrency_limit=8)
|
| 201 |
def lora_dataset_artifact(session_json: str = "{}") -> str:
|
| 202 |
-
|
| 203 |
-
session = json.loads(session_json or "{}")
|
| 204 |
-
except json.JSONDecodeError:
|
| 205 |
-
session = {}
|
| 206 |
return build_lora_dataset_jsonl(
|
| 207 |
session,
|
| 208 |
{
|
|
@@ -214,10 +283,7 @@ def lora_dataset_artifact(session_json: str = "{}") -> str:
|
|
| 214 |
|
| 215 |
@app.api(name="submission_packet", concurrency_limit=8)
|
| 216 |
def submission_packet_artifact(session_json: str = "{}") -> str:
|
| 217 |
-
|
| 218 |
-
session = json.loads(session_json or "{}")
|
| 219 |
-
except json.JSONDecodeError:
|
| 220 |
-
session = {}
|
| 221 |
runtime_status = engine.runtime_status()
|
| 222 |
return build_submission_packet_markdown(
|
| 223 |
session,
|
|
@@ -231,36 +297,7 @@ def submission_packet_artifact(session_json: str = "{}") -> str:
|
|
| 231 |
|
| 232 |
@app.api(name="agent_turn", concurrency_limit=4, stream_every=0.04)
|
| 233 |
def agent_turn(message: str, session_json: str = "{}") -> Iterator[str]:
|
| 234 |
-
|
| 235 |
-
session = json.loads(session_json or "{}")
|
| 236 |
-
except json.JSONDecodeError:
|
| 237 |
-
session = {}
|
| 238 |
-
|
| 239 |
-
result = _engine_turn(message, session)
|
| 240 |
-
yield _json_event(
|
| 241 |
-
{
|
| 242 |
-
"type": "start",
|
| 243 |
-
"corrections": [correction.to_dict() for correction in result.corrections],
|
| 244 |
-
"normalized_text": result.normalized_text,
|
| 245 |
-
"tool_events": [event.to_dict() for event in result.tool_events],
|
| 246 |
-
}
|
| 247 |
-
)
|
| 248 |
-
|
| 249 |
-
for chunk in result.stream_chunks():
|
| 250 |
-
yield _json_event({"type": "token", "text": chunk})
|
| 251 |
-
|
| 252 |
-
yield _json_event(
|
| 253 |
-
{
|
| 254 |
-
"type": "done",
|
| 255 |
-
"state": result.state,
|
| 256 |
-
"response": result.response,
|
| 257 |
-
"projects": [project.to_public_dict() for project in result.projects],
|
| 258 |
-
"whitespace": [item.to_dict() for item in result.whitespace],
|
| 259 |
-
"score": result.score.to_dict() if result.score else None,
|
| 260 |
-
"plan": result.plan,
|
| 261 |
-
"artifact": result.artifact,
|
| 262 |
-
}
|
| 263 |
-
)
|
| 264 |
|
| 265 |
|
| 266 |
if __name__ == "__main__":
|
|
|
|
| 6 |
from typing import Any, Iterator
|
| 7 |
|
| 8 |
from fastapi import Body
|
| 9 |
+
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, Response, StreamingResponse
|
| 10 |
from gradio import Server
|
| 11 |
|
| 12 |
from hackathon_advisor.agent import AdvisorEngine
|
|
|
|
| 46 |
return engine.turn(message, session)
|
| 47 |
|
| 48 |
|
| 49 |
+
def _session_from_json(session_json: str = "{}") -> dict[str, Any]:
|
| 50 |
+
try:
|
| 51 |
+
session = json.loads(session_json or "{}")
|
| 52 |
+
except json.JSONDecodeError:
|
| 53 |
+
return {}
|
| 54 |
+
return session if isinstance(session, dict) else {}
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _session_from_payload(payload: dict[str, Any] | None) -> dict[str, Any]:
|
| 58 |
+
payload = payload or {}
|
| 59 |
+
return _session_from_json(str(payload.get("session_json") or "{}"))
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def _agent_turn_events(message: str, session_json: str = "{}") -> Iterator[str]:
|
| 63 |
+
session = _session_from_json(session_json)
|
| 64 |
+
result = _engine_turn(message, session)
|
| 65 |
+
yield _json_event(
|
| 66 |
+
{
|
| 67 |
+
"type": "start",
|
| 68 |
+
"corrections": [correction.to_dict() for correction in result.corrections],
|
| 69 |
+
"normalized_text": result.normalized_text,
|
| 70 |
+
"tool_events": [event.to_dict() for event in result.tool_events],
|
| 71 |
+
}
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
for chunk in result.stream_chunks():
|
| 75 |
+
yield _json_event({"type": "token", "text": chunk})
|
| 76 |
+
|
| 77 |
+
yield _json_event(
|
| 78 |
+
{
|
| 79 |
+
"type": "done",
|
| 80 |
+
"state": result.state,
|
| 81 |
+
"response": result.response,
|
| 82 |
+
"projects": [project.to_public_dict() for project in result.projects],
|
| 83 |
+
"whitespace": [item.to_dict() for item in result.whitespace],
|
| 84 |
+
"score": result.score.to_dict() if result.score else None,
|
| 85 |
+
"plan": result.plan,
|
| 86 |
+
"artifact": result.artifact,
|
| 87 |
+
}
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
|
| 91 |
@app.get("/", response_class=HTMLResponse)
|
| 92 |
def home() -> FileResponse:
|
| 93 |
return FileResponse(STATIC_DIR / "index.html")
|
|
|
|
| 177 |
)
|
| 178 |
|
| 179 |
|
| 180 |
+
@app.post("/api/agent-turn")
|
| 181 |
+
def agent_turn_stream(payload: dict[str, Any] | None = Body(default=None)) -> StreamingResponse:
|
| 182 |
+
payload = payload or {}
|
| 183 |
+
message = str(payload.get("message") or "")
|
| 184 |
+
session_json = str(payload.get("session_json") or "{}")
|
| 185 |
+
|
| 186 |
+
def stream() -> Iterator[str]:
|
| 187 |
+
for event in _agent_turn_events(message, session_json):
|
| 188 |
+
yield f"{event}\n"
|
| 189 |
+
|
| 190 |
+
return StreamingResponse(stream(), media_type="application/x-ndjson")
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
@app.post("/api/field-notes")
|
| 194 |
+
def field_notes_api(payload: dict[str, Any] | None = Body(default=None)) -> Response:
|
| 195 |
+
session = _session_from_payload(payload)
|
| 196 |
+
content = build_field_notes_markdown(
|
| 197 |
+
session,
|
| 198 |
+
{
|
| 199 |
+
**trace_metadata(index),
|
| 200 |
+
"project_count": len(index.projects),
|
| 201 |
+
},
|
| 202 |
+
)
|
| 203 |
+
return Response(content=content, media_type="text/markdown; charset=utf-8")
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
@app.post("/api/chapter")
|
| 207 |
+
def chapter_api(payload: dict[str, Any] | None = Body(default=None)) -> Response:
|
| 208 |
+
session = _session_from_payload(payload)
|
| 209 |
+
content = build_chapter_markdown(
|
| 210 |
+
session,
|
| 211 |
+
{
|
| 212 |
+
**trace_metadata(index),
|
| 213 |
+
"project_count": len(index.projects),
|
| 214 |
+
},
|
| 215 |
+
)
|
| 216 |
+
return Response(content=content, media_type="text/markdown; charset=utf-8")
|
| 217 |
+
|
| 218 |
+
|
| 219 |
@app.get("/api/lora-training-kit.zip")
|
| 220 |
def lora_training_kit() -> Response:
|
| 221 |
runtime_status = engine.runtime_status()
|
|
|
|
| 241 |
|
| 242 |
@app.api(name="trace_artifact", concurrency_limit=8)
|
| 243 |
def trace_artifact(session_json: str = "{}") -> str:
|
| 244 |
+
session = _session_from_json(session_json)
|
|
|
|
|
|
|
|
|
|
| 245 |
return build_trace_jsonl(session, trace_metadata(index))
|
| 246 |
|
| 247 |
|
| 248 |
@app.api(name="field_notes", concurrency_limit=8)
|
| 249 |
def field_notes_artifact(session_json: str = "{}") -> str:
|
| 250 |
+
session = _session_from_json(session_json)
|
|
|
|
|
|
|
|
|
|
| 251 |
return build_field_notes_markdown(
|
| 252 |
session,
|
| 253 |
{
|
|
|
|
| 259 |
|
| 260 |
@app.api(name="chapter", concurrency_limit=8)
|
| 261 |
def chapter_artifact(session_json: str = "{}") -> str:
|
| 262 |
+
session = _session_from_json(session_json)
|
|
|
|
|
|
|
|
|
|
| 263 |
return build_chapter_markdown(
|
| 264 |
session,
|
| 265 |
{
|
|
|
|
| 271 |
|
| 272 |
@app.api(name="lora_dataset", concurrency_limit=8)
|
| 273 |
def lora_dataset_artifact(session_json: str = "{}") -> str:
|
| 274 |
+
session = _session_from_json(session_json)
|
|
|
|
|
|
|
|
|
|
| 275 |
return build_lora_dataset_jsonl(
|
| 276 |
session,
|
| 277 |
{
|
|
|
|
| 283 |
|
| 284 |
@app.api(name="submission_packet", concurrency_limit=8)
|
| 285 |
def submission_packet_artifact(session_json: str = "{}") -> str:
|
| 286 |
+
session = _session_from_json(session_json)
|
|
|
|
|
|
|
|
|
|
| 287 |
runtime_status = engine.runtime_status()
|
| 288 |
return build_submission_packet_markdown(
|
| 289 |
session,
|
|
|
|
| 297 |
|
| 298 |
@app.api(name="agent_turn", concurrency_limit=4, stream_every=0.04)
|
| 299 |
def agent_turn(message: str, session_json: str = "{}") -> Iterator[str]:
|
| 300 |
+
yield from _agent_turn_events(message, session_json)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
|
| 302 |
|
| 303 |
if __name__ == "__main__":
|
static/app.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
| 1 |
-
import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
|
| 2 |
-
|
| 3 |
const form = document.querySelector("#turn-form");
|
| 4 |
const input = document.querySelector("#message");
|
| 5 |
const submit = document.querySelector("#submit");
|
|
@@ -35,7 +33,6 @@ const CHAPTER_FILENAME = "hackathon-advisor-chapter.md";
|
|
| 35 |
const PNG_EXPORT_LABEL = "PNG";
|
| 36 |
|
| 37 |
let session = {};
|
| 38 |
-
let clientPromise = Client.connect(window.location.origin);
|
| 39 |
let currentArtifact = null;
|
| 40 |
let goalOptions = [];
|
| 41 |
let goalProfiles = [];
|
|
@@ -150,18 +147,19 @@ async function runTurn(message) {
|
|
| 150 |
|
| 151 |
let completed = false;
|
| 152 |
try {
|
| 153 |
-
const
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
| 157 |
});
|
|
|
|
|
|
|
| 158 |
|
| 159 |
-
for await (const
|
| 160 |
-
|
| 161 |
-
const payloads = Array.isArray(event.data) ? event.data : [event.data];
|
| 162 |
-
for (const raw of payloads) {
|
| 163 |
-
handleEvent(JSON.parse(raw));
|
| 164 |
-
}
|
| 165 |
}
|
| 166 |
completed = true;
|
| 167 |
} catch (error) {
|
|
@@ -179,6 +177,29 @@ async function runTurn(message) {
|
|
| 179 |
return completed;
|
| 180 |
}
|
| 181 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
async function runCommand(command) {
|
| 183 |
if (!command) return;
|
| 184 |
const draft = input.value.trim();
|
|
@@ -993,7 +1014,7 @@ function setSessionStatus(message) {
|
|
| 993 |
|
| 994 |
async function exportNotes() {
|
| 995 |
await exportMarkdown({
|
| 996 |
-
endpoint: "/
|
| 997 |
filename: FIELD_NOTES_FILENAME,
|
| 998 |
button: exportNotesButton,
|
| 999 |
busyLabel: "Notes...",
|
|
@@ -1004,7 +1025,7 @@ async function exportNotes() {
|
|
| 1004 |
|
| 1005 |
async function exportChapter() {
|
| 1006 |
await exportMarkdown({
|
| 1007 |
-
endpoint: "/chapter",
|
| 1008 |
filename: CHAPTER_FILENAME,
|
| 1009 |
button: exportChapterButton,
|
| 1010 |
busyLabel: "Chapter...",
|
|
@@ -1023,12 +1044,15 @@ async function exportMarkdown({ endpoint, filename, button, busyLabel, pendingLa
|
|
| 1023 |
corrections.textContent = session.ui_status;
|
| 1024 |
saveSession();
|
| 1025 |
try {
|
| 1026 |
-
const
|
| 1027 |
-
|
| 1028 |
-
|
|
|
|
|
|
|
|
|
|
| 1029 |
});
|
| 1030 |
-
|
| 1031 |
-
const text =
|
| 1032 |
if (!text.trim()) throw new Error("empty export");
|
| 1033 |
if (!isCurrentSessionRevision(revision)) return;
|
| 1034 |
downloadText(filename, text, "text/markdown;charset=utf-8");
|
|
|
|
|
|
|
|
|
|
| 1 |
const form = document.querySelector("#turn-form");
|
| 2 |
const input = document.querySelector("#message");
|
| 3 |
const submit = document.querySelector("#submit");
|
|
|
|
| 33 |
const PNG_EXPORT_LABEL = "PNG";
|
| 34 |
|
| 35 |
let session = {};
|
|
|
|
| 36 |
let currentArtifact = null;
|
| 37 |
let goalOptions = [];
|
| 38 |
let goalProfiles = [];
|
|
|
|
| 147 |
|
| 148 |
let completed = false;
|
| 149 |
try {
|
| 150 |
+
const response = await fetch("/api/agent-turn", {
|
| 151 |
+
method: "POST",
|
| 152 |
+
headers: { "Content-Type": "application/json" },
|
| 153 |
+
body: JSON.stringify({
|
| 154 |
+
message,
|
| 155 |
+
session_json: JSON.stringify(session),
|
| 156 |
+
}),
|
| 157 |
});
|
| 158 |
+
if (!response.ok) throw new Error(`advisor failed with ${response.status}`);
|
| 159 |
+
if (!response.body) throw new Error("advisor stream was empty");
|
| 160 |
|
| 161 |
+
for await (const raw of readNdjson(response.body)) {
|
| 162 |
+
handleEvent(JSON.parse(raw));
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
}
|
| 164 |
completed = true;
|
| 165 |
} catch (error) {
|
|
|
|
| 177 |
return completed;
|
| 178 |
}
|
| 179 |
|
| 180 |
+
async function* readNdjson(stream) {
|
| 181 |
+
const reader = stream.getReader();
|
| 182 |
+
const decoder = new TextDecoder();
|
| 183 |
+
let buffer = "";
|
| 184 |
+
|
| 185 |
+
while (true) {
|
| 186 |
+
const { value, done } = await reader.read();
|
| 187 |
+
if (done) break;
|
| 188 |
+
buffer += decoder.decode(value, { stream: true });
|
| 189 |
+
let newlineIndex = buffer.indexOf("\n");
|
| 190 |
+
while (newlineIndex >= 0) {
|
| 191 |
+
const line = buffer.slice(0, newlineIndex).trim();
|
| 192 |
+
buffer = buffer.slice(newlineIndex + 1);
|
| 193 |
+
if (line) yield line;
|
| 194 |
+
newlineIndex = buffer.indexOf("\n");
|
| 195 |
+
}
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
buffer += decoder.decode();
|
| 199 |
+
const finalLine = buffer.trim();
|
| 200 |
+
if (finalLine) yield finalLine;
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
async function runCommand(command) {
|
| 204 |
if (!command) return;
|
| 205 |
const draft = input.value.trim();
|
|
|
|
| 1014 |
|
| 1015 |
async function exportNotes() {
|
| 1016 |
await exportMarkdown({
|
| 1017 |
+
endpoint: "/api/field-notes",
|
| 1018 |
filename: FIELD_NOTES_FILENAME,
|
| 1019 |
button: exportNotesButton,
|
| 1020 |
busyLabel: "Notes...",
|
|
|
|
| 1025 |
|
| 1026 |
async function exportChapter() {
|
| 1027 |
await exportMarkdown({
|
| 1028 |
+
endpoint: "/api/chapter",
|
| 1029 |
filename: CHAPTER_FILENAME,
|
| 1030 |
button: exportChapterButton,
|
| 1031 |
busyLabel: "Chapter...",
|
|
|
|
| 1044 |
corrections.textContent = session.ui_status;
|
| 1045 |
saveSession();
|
| 1046 |
try {
|
| 1047 |
+
const response = await fetch(endpoint, {
|
| 1048 |
+
method: "POST",
|
| 1049 |
+
headers: { "Content-Type": "application/json" },
|
| 1050 |
+
body: JSON.stringify({
|
| 1051 |
+
session_json: JSON.stringify(session),
|
| 1052 |
+
}),
|
| 1053 |
});
|
| 1054 |
+
if (!response.ok) throw new Error(`export failed with ${response.status}`);
|
| 1055 |
+
const text = await response.text();
|
| 1056 |
if (!text.trim()) throw new Error("empty export");
|
| 1057 |
if (!isCurrentSessionRevision(revision)) return;
|
| 1058 |
downloadText(filename, text, "text/markdown;charset=utf-8");
|
tests/test_app.py
CHANGED
|
@@ -1,14 +1,18 @@
|
|
| 1 |
import json
|
|
|
|
| 2 |
from io import BytesIO
|
| 3 |
from zipfile import ZipFile
|
| 4 |
|
| 5 |
from app import (
|
|
|
|
| 6 |
artifact_png,
|
| 7 |
bootstrap,
|
|
|
|
| 8 |
chapter_artifact,
|
| 9 |
demo_bundle,
|
| 10 |
demo_session,
|
| 11 |
engine,
|
|
|
|
| 12 |
field_notes_artifact,
|
| 13 |
health,
|
| 14 |
index,
|
|
@@ -23,6 +27,13 @@ from app import (
|
|
| 23 |
)
|
| 24 |
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
def test_health_exposes_index_metadata() -> None:
|
| 27 |
payload = health()
|
| 28 |
|
|
@@ -50,6 +61,35 @@ def test_bootstrap_exposes_index_metadata() -> None:
|
|
| 50 |
assert all("trace" not in goal["description"].lower() for goal in payload["goal_profiles"])
|
| 51 |
|
| 52 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
def test_trace_artifact_endpoint_exports_jsonl() -> None:
|
| 54 |
state = engine.turn("A local-first archive cartographer for family photos", {}).state
|
| 55 |
payload = trace_artifact(json.dumps(state))
|
|
|
|
| 1 |
import json
|
| 2 |
+
import asyncio
|
| 3 |
from io import BytesIO
|
| 4 |
from zipfile import ZipFile
|
| 5 |
|
| 6 |
from app import (
|
| 7 |
+
agent_turn_stream,
|
| 8 |
artifact_png,
|
| 9 |
bootstrap,
|
| 10 |
+
chapter_api,
|
| 11 |
chapter_artifact,
|
| 12 |
demo_bundle,
|
| 13 |
demo_session,
|
| 14 |
engine,
|
| 15 |
+
field_notes_api,
|
| 16 |
field_notes_artifact,
|
| 17 |
health,
|
| 18 |
index,
|
|
|
|
| 27 |
)
|
| 28 |
|
| 29 |
|
| 30 |
+
async def _read_streaming_response(response) -> str:
|
| 31 |
+
chunks = []
|
| 32 |
+
async for chunk in response.body_iterator:
|
| 33 |
+
chunks.append(chunk.decode("utf-8") if isinstance(chunk, bytes) else chunk)
|
| 34 |
+
return "".join(chunks)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
def test_health_exposes_index_metadata() -> None:
|
| 38 |
payload = health()
|
| 39 |
|
|
|
|
| 61 |
assert all("trace" not in goal["description"].lower() for goal in payload["goal_profiles"])
|
| 62 |
|
| 63 |
|
| 64 |
+
def test_agent_turn_stream_endpoint_exports_ndjson_events() -> None:
|
| 65 |
+
response = agent_turn_stream(
|
| 66 |
+
{
|
| 67 |
+
"message": "A local-first archive cartographer for family photos",
|
| 68 |
+
"session_json": "{}",
|
| 69 |
+
}
|
| 70 |
+
)
|
| 71 |
+
payload = asyncio.run(_read_streaming_response(response))
|
| 72 |
+
lines = [json.loads(line) for line in payload.splitlines()]
|
| 73 |
+
|
| 74 |
+
assert response.media_type == "application/x-ndjson"
|
| 75 |
+
assert lines[0]["type"] == "start"
|
| 76 |
+
assert any(line["type"] == "token" for line in lines)
|
| 77 |
+
assert lines[-1]["type"] == "done"
|
| 78 |
+
assert lines[-1]["state"]["ideas"]
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def test_markdown_api_endpoints_return_plain_markdown() -> None:
|
| 82 |
+
state = engine.turn("A local-first archive cartographer for family photos", {}).state
|
| 83 |
+
|
| 84 |
+
notes = field_notes_api({"session_json": json.dumps(state)})
|
| 85 |
+
chapter = chapter_api({"session_json": json.dumps(state)})
|
| 86 |
+
|
| 87 |
+
assert notes.media_type == "text/markdown; charset=utf-8"
|
| 88 |
+
assert notes.body.decode("utf-8").startswith("# Hackathon Advisor Field Notes")
|
| 89 |
+
assert chapter.media_type == "text/markdown; charset=utf-8"
|
| 90 |
+
assert chapter.body.decode("utf-8").startswith("# The Unwritten Almanac Chapter")
|
| 91 |
+
|
| 92 |
+
|
| 93 |
def test_trace_artifact_endpoint_exports_jsonl() -> None:
|
| 94 |
state = engine.turn("A local-first archive cartographer for family photos", {}).state
|
| 95 |
payload = trace_artifact(json.dumps(state))
|
tests/test_frontend_copy.py
CHANGED
|
@@ -12,6 +12,9 @@ def test_main_interface_copy_is_builder_facing() -> None:
|
|
| 12 |
assert "Loading an example idea board." in app_js
|
| 13 |
assert "Example idea board loaded with a plan and share page." in app_js
|
| 14 |
assert "/api/artifact.png" in app_js
|
|
|
|
|
|
|
|
|
|
| 15 |
assert "renderArtifactCanvas" not in app_js
|
| 16 |
assert "canvas.toDataURL" not in app_js
|
| 17 |
assert 'aria-label="Export build notes"' in html
|
|
|
|
| 12 |
assert "Loading an example idea board." in app_js
|
| 13 |
assert "Example idea board loaded with a plan and share page." in app_js
|
| 14 |
assert "/api/artifact.png" in app_js
|
| 15 |
+
assert "/api/agent-turn" in app_js
|
| 16 |
+
assert "readNdjson" in app_js
|
| 17 |
+
assert "@gradio/client" not in app_js
|
| 18 |
assert "renderArtifactCanvas" not in app_js
|
| 19 |
assert "canvas.toDataURL" not in app_js
|
| 20 |
assert 'aria-label="Export build notes"' in html
|