-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.py
More file actions
128 lines (104 loc) · 4.02 KB
/
Copy pathserver.py
File metadata and controls
128 lines (104 loc) · 4.02 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
"""LoopGuard streaming server.
Exposes the same runs as the CLI, but over HTTP/WebSocket so the React Flow UI can
consume the trace live:
GET /graph?scenario=... -> static graph topology (nodes + edges)
GET /runtime -> selected model and embedding providers
WS /run?scenario=... -> live event/signal/decision/alert stream
Run: uvicorn server:app --reload
"""
from __future__ import annotations
import asyncio
import os
from dotenv import load_dotenv
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from loopguard.detectors import LoopDetector
from loopguard.evals import evaluate, loop_dataset
from loopguard.runner import stream_run
from loopguard.scenarios import get_scenario
load_dotenv()
app = FastAPI(title="LoopGuard")
# The UI runs on a different origin (Vite dev server), so allow cross-origin access.
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
# How long to pause between streamed messages, so the UI animation is watchable.
STEP_DELAY_SECONDS = 0.5
REACT_SCENARIOS = {
"controlled_progress",
"controlled_503",
"controlled_401",
"controlled_empty",
"calc",
"trap",
}
def react_topology() -> dict:
return {
"nodes": [{"id": "agent"}, {"id": "tools"}],
"edges": [{"source": "agent", "target": "tools"}, {"source": "tools", "target": "agent"}],
}
def graph_topology(agent) -> dict:
"""Extract nodes + edges from a compiled LangGraph for React Flow to render."""
internal = ("__start__", "__end__")
try:
g = agent.get_graph()
nodes = [{"id": n} for n in g.nodes if n not in internal]
edges = [
{"source": e.source, "target": e.target}
for e in g.edges
if e.source not in internal and e.target not in internal
]
return {"nodes": nodes, "edges": edges}
except Exception: # noqa: BLE001 - fall back to the known shape if introspection changes
return {
"nodes": [{"id": "agent"}, {"id": "tools"}],
"edges": [{"source": "agent", "target": "tools"}, {"source": "tools", "target": "agent"}],
}
@app.get("/graph")
def graph(scenario: str = "exact") -> dict:
if scenario in REACT_SCENARIOS:
return react_topology()
_, agent, _, _ = get_scenario(scenario)
return graph_topology(agent)
@app.get("/eval")
def eval_detectors() -> dict:
"""Grade the LoopDetector on the labeled dataset and return its scorecard for the UI."""
cases = loop_dataset()
card = evaluate(cases, [LoopDetector(threshold=3)])
return {
"detector": "LoopDetector",
"cases": len(cases),
"tp": card.tp, "fp": card.fp, "fn": card.fn, "tn": card.tn,
"precision": round(card.precision, 2),
"recall": round(card.recall, 2),
"f1": round(card.f1, 2),
}
@app.get("/runtime")
def runtime() -> dict:
"""Return non-secret runtime provider settings for the UI."""
provider = os.getenv("LOOPGUARD_MODEL_PROVIDER", "openai").strip().lower()
if provider == "gemini":
model = os.getenv("LOOPGUARD_GEMINI_MODEL", "gemini-3.6-flash")
else:
model = os.getenv("LOOPGUARD_OPENAI_MODEL", "gpt-4o-mini")
return {
"model_provider": provider,
"model": model,
"embedding_provider": os.getenv("LOOPGUARD_EMBEDDING_PROVIDER", "openai"),
}
@app.websocket("/run")
async def run(websocket: WebSocket) -> None:
await websocket.accept()
scenario = websocket.query_params.get("scenario", "exact")
recursion_limit = 30 if scenario in {"calc", "trap"} else 50
try:
_, agent, detectors, initial = get_scenario(scenario)
for message in stream_run(agent, detectors, initial, recursion_limit):
await websocket.send_json(message)
await asyncio.sleep(STEP_DELAY_SECONDS)
await websocket.close()
except WebSocketDisconnect:
pass # client navigated away mid-run; nothing to clean up