Spaces:
Runtime error
Runtime error
bugs & error fix πΏ
Browse files- graph.py +11 -62
- static/app.js +4 -6
graph.py
CHANGED
|
@@ -15,7 +15,7 @@ from langgraph.graph import StateGraph, START, END
|
|
| 15 |
from langgraph.graph.message import add_messages
|
| 16 |
from langchain_core.messages import BaseMessage, SystemMessage, HumanMessage, AIMessage
|
| 17 |
from langchain_core.runnables import RunnableConfig
|
| 18 |
-
from langchain_openrouter import ChatOpenRouter
|
| 19 |
from typing import TypedDict, Annotated
|
| 20 |
|
| 21 |
import sqlite3
|
|
@@ -23,9 +23,7 @@ import time
|
|
| 23 |
import json
|
| 24 |
import urllib.request
|
| 25 |
import threading
|
| 26 |
-
import
|
| 27 |
-
import concurrent.futures
|
| 28 |
-
from langgraph.checkpoint.sqlite import SqliteSaver
|
| 29 |
from config import OPENROUTER_API_KEY, DB_PATH, LLM_TIMEOUT
|
| 30 |
import prompts
|
| 31 |
|
|
@@ -284,43 +282,22 @@ def _strip_images(messages: list) -> list:
|
|
| 284 |
return out
|
| 285 |
|
| 286 |
|
| 287 |
-
# ββ LLM factory
|
| 288 |
-
#
|
| 289 |
-
# ChatOpenRouter wraps the `openrouter` Python SDK which creates its
|
| 290 |
-
# own httpx.Client internally. That client does NOT inherit the
|
| 291 |
-
# `timeout` kwarg passed to ChatOpenRouter at the LangChain level.
|
| 292 |
-
#
|
| 293 |
-
# Fix: run llm.invoke() in a daemon thread and enforce a hard
|
| 294 |
-
# Python-level deadline with future.result(timeout=LLM_TIMEOUT).
|
| 295 |
-
# This kills the blocked thread from Python's perspective and raises
|
| 296 |
-
# TimeoutError which our retry loop handles like any other error.
|
| 297 |
-
|
| 298 |
-
# Thread pool β reused across calls to avoid spawn overhead
|
| 299 |
-
_executor = concurrent.futures.ThreadPoolExecutor(max_workers=4)
|
| 300 |
-
|
| 301 |
|
| 302 |
def _make_llm(api_key: str, model_id: str):
|
| 303 |
key = api_key or OPENROUTER_API_KEY
|
| 304 |
if not key:
|
| 305 |
-
raise ValueError(
|
| 306 |
-
"No API key available. Please add your OpenRouter key in Settings."
|
| 307 |
-
)
|
| 308 |
return ChatOpenRouter(
|
| 309 |
model=model_id,
|
| 310 |
openrouter_api_key=key,
|
| 311 |
temperature=0.5,
|
| 312 |
max_tokens=4096,
|
| 313 |
-
max_retries=
|
| 314 |
streaming=True,
|
| 315 |
)
|
| 316 |
|
| 317 |
|
| 318 |
-
def _invoke_with_timeout(llm, messages: list, timeout: float):
|
| 319 |
-
"""Run llm.invoke in a thread; raise TimeoutError after `timeout` seconds."""
|
| 320 |
-
future = _executor.submit(llm.invoke, messages)
|
| 321 |
-
return future.result(timeout=timeout)
|
| 322 |
-
|
| 323 |
-
|
| 324 |
# ββ LangGraph state & node ββββββββββββββββββββββββββββββββββββ
|
| 325 |
|
| 326 |
class ChatState(TypedDict):
|
|
@@ -338,62 +315,33 @@ def chat_node(state: ChatState, config: RunnableConfig):
|
|
| 338 |
override = cfg.get("model", "")
|
| 339 |
has_image = cfg.get("has_image", False)
|
| 340 |
|
| 341 |
-
# 1) Classify
|
| 342 |
user_text = _extract_text(state["messages"])
|
| 343 |
category = _classify(user_text)
|
| 344 |
|
| 345 |
-
# 2) Pick model
|
| 346 |
if override:
|
| 347 |
model_id, actual = override, category
|
| 348 |
else:
|
| 349 |
model_id, actual = _pick(category, has_image=has_image)
|
| 350 |
|
| 351 |
-
# 3) Strip images from history when model is not vision-capable
|
| 352 |
messages = state["messages"]
|
| 353 |
if actual != "vision":
|
| 354 |
messages = _strip_images(messages)
|
| 355 |
|
| 356 |
-
# 4) Build system prompt
|
| 357 |
base_prompt = prompts.build(persona, context, language, username, profile)
|
| 358 |
|
| 359 |
if has_image:
|
| 360 |
base_prompt += (
|
| 361 |
"\n\nIMAGE HANDLING:\n"
|
| 362 |
-
"The student
|
| 363 |
-
"
|
| 364 |
-
"Describe what you see in the image if relevant, then answer their query. "
|
| 365 |
-
"Do NOT output safety classifications, content ratings, or moderation labels. "
|
| 366 |
-
"You are a teacher β respond with an educational explanation, not a safety assessment.\n"
|
| 367 |
)
|
| 368 |
|
| 369 |
sys = SystemMessage(content=base_prompt)
|
| 370 |
print(f"[ROUTER] category={category} model={model_id} vision={actual == 'vision'}")
|
| 371 |
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
for attempt in range(max_attempts):
|
| 376 |
-
try:
|
| 377 |
-
llm = _make_llm(api_key, model_id)
|
| 378 |
-
resp = _invoke_with_timeout(llm, [sys] + messages, timeout=LLM_TIMEOUT)
|
| 379 |
-
return {"messages": [resp]}
|
| 380 |
-
except concurrent.futures.TimeoutError:
|
| 381 |
-
last_error = TimeoutError(f"Model {model_id} timed out after {LLM_TIMEOUT}s")
|
| 382 |
-
print(f"[ROUTER] TIMEOUT {model_id} after {LLM_TIMEOUT}s (attempt {attempt+1}/{max_attempts})")
|
| 383 |
-
if attempt < max_attempts - 1:
|
| 384 |
-
model_id, actual = _pick(category, has_image=has_image)
|
| 385 |
-
print(f"[ROUTER] Rotating to: {model_id}")
|
| 386 |
-
except Exception as e:
|
| 387 |
-
last_error = e
|
| 388 |
-
print(f"[ROUTER] FAIL {model_id} (attempt {attempt+1}/{max_attempts}): {type(e).__name__}: {str(e)[:200]}")
|
| 389 |
-
traceback.print_exc()
|
| 390 |
-
if attempt < max_attempts - 1:
|
| 391 |
-
model_id, actual = _pick(category, has_image=has_image)
|
| 392 |
-
print(f"[ROUTER] Rotating to: {model_id}")
|
| 393 |
-
|
| 394 |
-
# All attempts failed β raise so app.py SSE handler can show the error
|
| 395 |
-
print(f"[ROUTER] All {max_attempts} attempts failed. Raising last error.")
|
| 396 |
-
raise last_error
|
| 397 |
|
| 398 |
|
| 399 |
# ββ Compile graph βββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -404,3 +352,4 @@ _g.add_edge(START, "chat_node")
|
|
| 404 |
_g.add_edge("chat_node", END)
|
| 405 |
|
| 406 |
chatbot = _g.compile(checkpointer=checkpointer)
|
|
|
|
|
|
| 15 |
from langgraph.graph.message import add_messages
|
| 16 |
from langchain_core.messages import BaseMessage, SystemMessage, HumanMessage, AIMessage
|
| 17 |
from langchain_core.runnables import RunnableConfig
|
| 18 |
+
from langchain_openrouter import ChatOpenRouter #type:ignore
|
| 19 |
from typing import TypedDict, Annotated
|
| 20 |
|
| 21 |
import sqlite3
|
|
|
|
| 23 |
import json
|
| 24 |
import urllib.request
|
| 25 |
import threading
|
| 26 |
+
from langgraph.checkpoint.sqlite import SqliteSaver #type:ignore
|
|
|
|
|
|
|
| 27 |
from config import OPENROUTER_API_KEY, DB_PATH, LLM_TIMEOUT
|
| 28 |
import prompts
|
| 29 |
|
|
|
|
| 282 |
return out
|
| 283 |
|
| 284 |
|
| 285 |
+
# ββ LLM factory βββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 286 |
|
| 287 |
def _make_llm(api_key: str, model_id: str):
|
| 288 |
key = api_key or OPENROUTER_API_KEY
|
| 289 |
if not key:
|
| 290 |
+
raise ValueError("No API key. Add your OpenRouter key in Settings.")
|
|
|
|
|
|
|
| 291 |
return ChatOpenRouter(
|
| 292 |
model=model_id,
|
| 293 |
openrouter_api_key=key,
|
| 294 |
temperature=0.5,
|
| 295 |
max_tokens=4096,
|
| 296 |
+
max_retries=1,
|
| 297 |
streaming=True,
|
| 298 |
)
|
| 299 |
|
| 300 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
# ββ LangGraph state & node ββββββββββββββββββββββββββββββββββββ
|
| 302 |
|
| 303 |
class ChatState(TypedDict):
|
|
|
|
| 315 |
override = cfg.get("model", "")
|
| 316 |
has_image = cfg.get("has_image", False)
|
| 317 |
|
|
|
|
| 318 |
user_text = _extract_text(state["messages"])
|
| 319 |
category = _classify(user_text)
|
| 320 |
|
|
|
|
| 321 |
if override:
|
| 322 |
model_id, actual = override, category
|
| 323 |
else:
|
| 324 |
model_id, actual = _pick(category, has_image=has_image)
|
| 325 |
|
|
|
|
| 326 |
messages = state["messages"]
|
| 327 |
if actual != "vision":
|
| 328 |
messages = _strip_images(messages)
|
| 329 |
|
|
|
|
| 330 |
base_prompt = prompts.build(persona, context, language, username, profile)
|
| 331 |
|
| 332 |
if has_image:
|
| 333 |
base_prompt += (
|
| 334 |
"\n\nIMAGE HANDLING:\n"
|
| 335 |
+
"The student attached an image. Analyze it and respond educationally. "
|
| 336 |
+
"Do NOT output safety classifications or moderation labels.\n"
|
|
|
|
|
|
|
|
|
|
| 337 |
)
|
| 338 |
|
| 339 |
sys = SystemMessage(content=base_prompt)
|
| 340 |
print(f"[ROUTER] category={category} model={model_id} vision={actual == 'vision'}")
|
| 341 |
|
| 342 |
+
llm = _make_llm(api_key, model_id)
|
| 343 |
+
resp = llm.invoke([sys] + messages)
|
| 344 |
+
return {"messages": [resp]}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
|
| 346 |
|
| 347 |
# ββ Compile graph βββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 352 |
_g.add_edge("chat_node", END)
|
| 353 |
|
| 354 |
chatbot = _g.compile(checkpointer=checkpointer)
|
| 355 |
+
|
static/app.js
CHANGED
|
@@ -357,13 +357,11 @@ function showApp() {
|
|
| 357 |
|
| 358 |
function enterHeroMode() {
|
| 359 |
isHeroMode = true;
|
| 360 |
-
if (welcomeScreen) welcomeScreen.style.display = '';
|
| 361 |
bottomInputContainer.style.display = 'none';
|
| 362 |
-
const
|
| 363 |
-
if (
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
}
|
| 367 |
_updateHeroTitle();
|
| 368 |
}
|
| 369 |
|
|
|
|
| 357 |
|
| 358 |
function enterHeroMode() {
|
| 359 |
isHeroMode = true;
|
|
|
|
| 360 |
bottomInputContainer.style.display = 'none';
|
| 361 |
+
const old = document.getElementById('welcomeScreen');
|
| 362 |
+
if (old) old.remove();
|
| 363 |
+
chatContainer.innerHTML = '';
|
| 364 |
+
chatContainer.appendChild(createWelcomeScreen());
|
|
|
|
| 365 |
_updateHeroTitle();
|
| 366 |
}
|
| 367 |
|