CodeChamp95 commited on
Commit
aaa5147
Β·
verified Β·
1 Parent(s): 6389fcd

Update agent.py

Browse files
Files changed (1) hide show
  1. agent.py +186 -65
agent.py CHANGED
@@ -7,10 +7,14 @@ using LangGraph, ChatMistralAI, and MemorySaver.
7
  from __future__ import annotations
8
 
9
  import json
 
10
  import re
 
11
  from pathlib import Path
12
  from typing import Any, Generator
13
 
 
 
14
  from langchain_mistralai import ChatMistralAI
15
  from langgraph.checkpoint.memory import MemorySaver
16
  from langgraph.prebuilt import create_react_agent
@@ -41,6 +45,25 @@ _TAXONOMY = str(ARTIFACTS_DIR / "taxonomy_mapping.json")
41
  _COMPARISON_CSV = str(ARTIFACTS_DIR / "abstract_vs_title_comparison.csv")
42
  _NARRATIVE = str(ARTIFACTS_DIR / "section7_narrative.txt")
43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  # ──────────────────────────────────────────────────────────────────────────────
45
  # System Prompt
46
  # ──────────────────────────────────────────────────────────────────────────────
@@ -509,51 +532,105 @@ class BERTopicAgent:
509
  stream: Any,
510
  history: list,
511
  user_msg: str,
 
512
  ) -> Generator[tuple, None, None]:
513
  """
514
  Consume a LangGraph stream, yielding Gradio 5-tuples incrementally.
515
- Handles both streaming token chunks and full message events.
 
 
 
516
  """
517
  accumulated = ""
518
 
519
- for chunk in stream:
520
- # LangGraph yields dicts keyed by node name
521
- node_output = (
522
- chunk.get("agent") or
523
- chunk.get("tools") or
524
- {}
525
- )
526
- messages = node_output.get("messages", [])
527
-
528
- text_delta = "".join(list(map(
529
- lambda m: getattr(m, "content", "") if hasattr(m, "content") else "",
530
- messages,
531
- )))
532
-
533
- accumulated += text_delta
534
- self._update_charts(accumulated)
535
- self._update_downloads(accumulated)
536
- self.phase = _detect_phase(accumulated)
537
-
538
- updated_history = history + [[user_msg, accumulated]] if accumulated else history
539
-
 
 
 
 
 
 
 
 
 
 
 
540
  yield (
541
- updated_history,
542
  self.phase,
543
  dict(self._charts),
544
  list(self._downloads),
545
- [], # topic_rows populated in final yield
546
  )
547
 
548
- # Final yield with review-table rows (if artefacts exist)
549
- topic_rows = self._latest_review_rows()
550
- yield (
551
- history + [[user_msg, accumulated]],
552
- self.phase,
553
- dict(self._charts),
554
- list(self._downloads),
555
- topic_rows,
556
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
557
 
558
  def _latest_review_rows(self) -> list[list]:
559
  """Return review rows from the most recently produced artefact."""
@@ -624,37 +701,81 @@ class BERTopicAgent:
624
  f"Please continue to the next B&C phase now."
625
  )
626
 
627
- stream = self._graph.stream(
628
- {"messages": [{"role": "user", "content": review_message}]},
629
- config=self._config(),
630
- stream_mode="updates",
631
- )
632
-
633
- accumulated = ""
634
- for chunk in stream:
635
- node_output = chunk.get("agent") or chunk.get("tools") or {}
636
- messages = node_output.get("messages", [])
637
-
638
- text_delta = "".join(list(map(
639
- lambda m: getattr(m, "content", "") if hasattr(m, "content") else "",
640
- messages,
641
- )))
642
-
643
- accumulated += text_delta
644
- self._update_charts(accumulated)
645
- self._update_downloads(accumulated)
646
- self.phase = _detect_phase(accumulated)
647
-
648
- yield (
649
- history + [["[Review submitted]", accumulated]],
650
- self.phase,
651
- dict(self._charts),
652
- list(self._downloads),
653
  )
654
 
655
- yield (
656
- history + [["[Review submitted]", accumulated]],
657
- self.phase,
658
- dict(self._charts),
659
- list(self._downloads),
660
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  from __future__ import annotations
8
 
9
  import json
10
+ import logging
11
  import re
12
+ import time
13
  from pathlib import Path
14
  from typing import Any, Generator
15
 
16
+ logger = logging.getLogger(__name__)
17
+
18
  from langchain_mistralai import ChatMistralAI
19
  from langgraph.checkpoint.memory import MemorySaver
20
  from langgraph.prebuilt import create_react_agent
 
45
  _COMPARISON_CSV = str(ARTIFACTS_DIR / "abstract_vs_title_comparison.csv")
46
  _NARRATIVE = str(ARTIFACTS_DIR / "section7_narrative.txt")
47
 
48
+ # ──────────────────────────────────────────────────────────────────────────────
49
+ # Rate-limit retry config
50
+ # ──────────────────────────────────────────────────────────────────────────────
51
+
52
+ _RL_MAX_RETRIES = 4 # max automatic retries on 429
53
+ _RL_BACKOFF_SECS = [15, 30, 60, 120] # wait before each retry attempt
54
+
55
+
56
+ def _is_rate_limit(exc: Exception) -> bool:
57
+ """Return True when *exc* is an HTTP 429 rate-limit error from any client."""
58
+ # httpx.HTTPStatusError carries a .response attribute
59
+ resp = getattr(exc, "response", None)
60
+ if resp is not None and getattr(resp, "status_code", None) == 429:
61
+ return True
62
+ # Fallback: inspect the string representation (handles wrapped exceptions)
63
+ s = str(exc).lower()
64
+ return "429" in s and ("rate limit" in s or "rate_limit" in s or "rate_limited" in s)
65
+
66
+
67
  # ──────────────────────────────────────────────────────────────────────────────
68
  # System Prompt
69
  # ──────────────────────────────────────────────────────────────────────────────
 
532
  stream: Any,
533
  history: list,
534
  user_msg: str,
535
+ _attempt: int = 0,
536
  ) -> Generator[tuple, None, None]:
537
  """
538
  Consume a LangGraph stream, yielding Gradio 5-tuples incrementally.
539
+
540
+ Automatically retries on HTTP 429 (rate-limit) errors with exponential
541
+ back-off up to _RL_MAX_RETRIES times. All other exceptions surface a
542
+ friendly error message in the chat rather than crashing the generator.
543
  """
544
  accumulated = ""
545
 
546
+ try:
547
+ for chunk in stream:
548
+ # LangGraph yields dicts keyed by node name
549
+ node_output = (
550
+ chunk.get("agent") or
551
+ chunk.get("tools") or
552
+ {}
553
+ )
554
+ messages = node_output.get("messages", [])
555
+
556
+ text_delta = "".join(list(map(
557
+ lambda m: getattr(m, "content", "") if hasattr(m, "content") else "",
558
+ messages,
559
+ )))
560
+
561
+ accumulated += text_delta
562
+ self._update_charts(accumulated)
563
+ self._update_downloads(accumulated)
564
+ self.phase = _detect_phase(accumulated)
565
+
566
+ updated_history = history + [[user_msg, accumulated]] if accumulated else history
567
+
568
+ yield (
569
+ updated_history,
570
+ self.phase,
571
+ dict(self._charts),
572
+ list(self._downloads),
573
+ [], # topic_rows populated in final yield
574
+ )
575
+
576
+ # ── Success: final yield with review-table rows ───────────────────
577
+ topic_rows = self._latest_review_rows()
578
  yield (
579
+ history + [[user_msg, accumulated]],
580
  self.phase,
581
  dict(self._charts),
582
  list(self._downloads),
583
+ topic_rows,
584
  )
585
 
586
+ except Exception as exc: # noqa: BLE001
587
+ if _is_rate_limit(exc) and _attempt < _RL_MAX_RETRIES:
588
+ # ── Rate-limit: back off then restart the stream ──────────────
589
+ wait = _RL_BACKOFF_SECS[_attempt]
590
+ notice = (
591
+ f"\n\n⏳ **Mistral rate limit hit** β€” waiting **{wait}s** "
592
+ f"then retrying automatically "
593
+ f"(attempt {_attempt + 1}/{_RL_MAX_RETRIES})…"
594
+ )
595
+ logger.warning("Rate limit 429 on attempt %d; sleeping %ds", _attempt, wait)
596
+ yield (
597
+ history + [[user_msg, accumulated + notice]],
598
+ "idle",
599
+ dict(self._charts),
600
+ list(self._downloads),
601
+ [],
602
+ )
603
+ time.sleep(wait)
604
+
605
+ # Rebuild the stream β€” MemorySaver resumes from last checkpoint
606
+ new_stream = self._graph.stream(
607
+ {"messages": [{"role": "user", "content": user_msg}]},
608
+ config=self._config(),
609
+ stream_mode="updates",
610
+ )
611
+ yield from self._accumulate_stream(
612
+ new_stream, history, user_msg, _attempt=_attempt + 1
613
+ )
614
+
615
+ else:
616
+ # ── Non-retryable error: surface gracefully in chat ───────────
617
+ if _is_rate_limit(exc):
618
+ err_header = (
619
+ f"❌ **Rate limit persists after {_RL_MAX_RETRIES} retries.**\n"
620
+ "Please wait a few minutes before sending another message."
621
+ )
622
+ else:
623
+ err_header = f"❌ **API / tool error:** `{type(exc).__name__}: {exc}`"
624
+
625
+ logger.exception("Unhandled error in _accumulate_stream (attempt %d)", _attempt)
626
+ self.phase = "idle"
627
+ yield (
628
+ history + [[user_msg, accumulated + f"\n\n{err_header}"]],
629
+ "idle",
630
+ dict(self._charts),
631
+ list(self._downloads),
632
+ self._latest_review_rows(), # keep existing table intact
633
+ )
634
 
635
  def _latest_review_rows(self) -> list[list]:
636
  """Return review rows from the most recently produced artefact."""
 
701
  f"Please continue to the next B&C phase now."
702
  )
703
 
704
+ def _make_review_stream() -> Any:
705
+ return self._graph.stream(
706
+ {"messages": [{"role": "user", "content": review_message}]},
707
+ config=self._config(),
708
+ stream_mode="updates",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
709
  )
710
 
711
+ accumulated = ""
712
+ attempt = 0
713
+
714
+ while True:
715
+ current_stream = _make_review_stream()
716
+ try:
717
+ for chunk in current_stream:
718
+ node_output = chunk.get("agent") or chunk.get("tools") or {}
719
+ messages = node_output.get("messages", [])
720
+
721
+ text_delta = "".join(list(map(
722
+ lambda m: getattr(m, "content", "") if hasattr(m, "content") else "",
723
+ messages,
724
+ )))
725
+
726
+ accumulated += text_delta
727
+ self._update_charts(accumulated)
728
+ self._update_downloads(accumulated)
729
+ self.phase = _detect_phase(accumulated)
730
+
731
+ yield (
732
+ history + [["[Review submitted]", accumulated]],
733
+ self.phase,
734
+ dict(self._charts),
735
+ list(self._downloads),
736
+ )
737
+
738
+ # ── Success ───────────────────────────────────────────────────
739
+ yield (
740
+ history + [["[Review submitted]", accumulated]],
741
+ self.phase,
742
+ dict(self._charts),
743
+ list(self._downloads),
744
+ )
745
+ break # exit retry loop
746
+
747
+ except Exception as exc: # noqa: BLE001
748
+ if _is_rate_limit(exc) and attempt < _RL_MAX_RETRIES:
749
+ wait = _RL_BACKOFF_SECS[attempt]
750
+ notice = (
751
+ f"\n\n⏳ **Rate limit hit** β€” waiting **{wait}s** "
752
+ f"then retrying (attempt {attempt + 1}/{_RL_MAX_RETRIES})…"
753
+ )
754
+ logger.warning("Rate limit 429 in handle_review attempt %d; sleeping %ds", attempt, wait)
755
+ yield (
756
+ history + [["[Review submitted]", accumulated + notice]],
757
+ "idle",
758
+ dict(self._charts),
759
+ list(self._downloads),
760
+ )
761
+ time.sleep(wait)
762
+ attempt += 1
763
+ # Loop re-creates the stream via _make_review_stream()
764
+ else:
765
+ if _is_rate_limit(exc):
766
+ err = (
767
+ f"❌ **Rate limit persists after {_RL_MAX_RETRIES} retries.**\n"
768
+ "Please wait a few minutes before trying again."
769
+ )
770
+ else:
771
+ err = f"❌ **Error processing review:** `{type(exc).__name__}: {exc}`"
772
+
773
+ logger.exception("Unhandled error in handle_review (attempt %d)", attempt)
774
+ self.phase = "idle"
775
+ yield (
776
+ history + [["[Review submitted]", accumulated + f"\n\n{err}"]],
777
+ "idle",
778
+ dict(self._charts),
779
+ list(self._downloads),
780
+ )
781
+ break