Subh775 commited on
Commit
b711541
Β·
1 Parent(s): 7934004

bugs & error fix πŸ—Ώ

Browse files
Files changed (2) hide show
  1. graph.py +11 -62
  2. 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 traceback
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 & thread-timeout wrapper ─────────────────────
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=0, # disable SDK retries β€” our loop handles rotation
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 has attached an image with their question. "
363
- "Analyze the image carefully and respond to their question about it. "
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
- # 5) Invoke with model rotation + hard Python timeout
373
- max_attempts = 3
374
- last_error = None
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 existing = document.getElementById('welcomeScreen');
363
- if (!existing) {
364
- chatContainer.innerHTML = '';
365
- chatContainer.appendChild(createWelcomeScreen());
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