Lexai vovkes222 Claude Opus 4.6 commited on
Commit
40900ed
·
unverified ·
1 Parent(s): c63f734

fix: make stream_chat async to avoid event loop conflict with Gradio (#10)

Browse files

The sync _run_sync wrapper created a new event loop inside Gradio's
existing async context, causing the chat to hang. Now stream_chat is
a native async generator that directly consumes _run_pipeline.

Co-authored-by: overthelex <mcvovkes@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

Files changed (2) hide show
  1. app.py +36 -70
  2. tests/test_app.py +6 -0
app.py CHANGED
@@ -1,13 +1,11 @@
1
  """LMAF (Legal Multi-Agent Framework) Gradio chat app.
2
 
3
  Chat interface for multi-agent legal consultation pipeline.
4
- On prod (agents.legal.org.ua): runs the real pipeline with LLM + SecondLayer API.
5
- On HF Space: proxies to prod via gr.load().
6
  """
7
 
8
  from __future__ import annotations
9
 
10
- import asyncio
11
  import os
12
  import sys
13
  from pathlib import Path
@@ -37,105 +35,73 @@ async def _run_pipeline(question: str):
37
  """Run the multi-agent pipeline and yield status updates."""
38
  from lmaf.core.config import Config
39
  from lmaf.engine import LMAF
 
40
 
41
  config = Config.from_env()
42
- intern = LMAF(question, config)
43
 
44
  yield "Surveyor: аналізую правовий ландшафт..."
45
- result = await intern.surveyor.run(intern.state)
46
- intern._loop.survey_done = True
47
- if intern.state.survey_summary:
48
- yield f"**Огляд**: {intern.state.survey_summary[:300]}"
49
 
50
  yield "Planner: розробляю стратегію дослідження..."
51
- result = await intern.planner.run(intern.state)
52
- intern._loop.plan_done = True
53
- if intern.state.strategy.approach:
54
- yield f"**Стратегія**: {intern.state.strategy.approach[:300]}"
55
 
56
  for iteration in range(1, config.max_iterations + 1):
57
- intern.state.iteration = iteration
58
 
59
  yield f"Ітерація {iteration}: Orchestrator вирішує наступний крок..."
60
- orch_result = await intern.orchestrator.run(intern.state)
61
 
62
  if not orch_result.success:
63
- if intern._loop.consecutive_failures >= config.max_consecutive_failures:
64
  yield "Занадто багато послідовних помилок, зупиняюсь."
65
  break
66
  continue
67
 
68
- yield f"Researcher: шукаю судову практику та законодавство..."
69
- agent_result = await intern.researcher.run(
70
- intern.state, task_description=orch_result.summary
71
  )
72
  if agent_result.success:
73
  yield f"**Знайдено**: {agent_result.summary[:200]}"
74
 
75
  yield "Reviewer: верифікую докази та посилання..."
76
- review_result = await intern.reviewer.run(intern.state)
77
 
78
  if iteration % config.critic_every_n == 0:
79
  yield "Critic: аудит стратегії та повноти аналізу..."
80
- critic_result = await intern.critic.run(intern.state)
81
- intern._loop.last_critic_iteration = iteration
82
 
83
- for critique in intern.state.active_critiques():
84
  if critique.type == "strategy":
85
- await intern.planner.run(
86
- intern.state, revision_critique=critique.details
87
  )
88
- from lmaf.state.research_state import CritiqueStatus
89
  critique.status = CritiqueStatus.RESOLVED
90
 
91
  if "можна завершувати: True" in critic_result.summary:
92
  yield "Critic схвалив -- формую консультацію."
93
  break
94
 
95
- if not intern.state.open_questions() and not intern.state.active_critiques():
96
  yield "Всі питання вирішено -- формую консультацію."
97
  break
98
 
99
  yield "Formatter: оформлюю фінальну консультацію..."
100
- await intern.formatter.run(intern.state)
101
 
102
- yield intern.state.answer or "Не вдалося сформувати відповідь."
103
 
104
 
105
- def chat_fn(message: str, history: list[dict]) -> str:
106
- """Synchronous wrapper that runs the async pipeline."""
107
- if not message.strip():
108
- return "Будь ласка, опишіть вашу правову ситуацію."
109
-
110
- if not has_api_keys():
111
- return (
112
- "API ключі не налаштовано. Цей інстанс працює в демо-режимі.\n\n"
113
- "Для реальних консультацій використовуйте "
114
- "[agents.legal.org.ua](https://agents.legal.org.ua)."
115
- )
116
-
117
- final = ""
118
- for update in _run_sync(message):
119
- final = update
120
- return final
121
-
122
-
123
- def _run_sync(question: str):
124
- """Run async generator synchronously, collecting results."""
125
- loop = asyncio.new_event_loop()
126
- gen = _run_pipeline(question)
127
- try:
128
- while True:
129
- result = loop.run_until_complete(gen.__anext__())
130
- yield result
131
- except StopAsyncIteration:
132
- pass
133
- finally:
134
- loop.close()
135
-
136
-
137
- def stream_chat(message: str, history: list[dict]):
138
- """Streaming chat handler -- yields incremental updates."""
139
  if not message.strip():
140
  yield "Будь ласка, опишіть вашу правову ситуацію."
141
  return
@@ -149,17 +115,17 @@ def stream_chat(message: str, history: list[dict]):
149
  return
150
 
151
  accumulated = ""
152
- for update in _run_sync(message):
 
 
 
 
153
  if update.startswith("**") or update.startswith("#"):
154
  accumulated += f"\n\n{update}"
155
- elif not update.startswith("Surveyor") and not update.startswith("Planner") \
156
- and not update.startswith("Ітерація") and not update.startswith("Researcher") \
157
- and not update.startswith("Reviewer") and not update.startswith("Critic") \
158
- and not update.startswith("Formatter") and not update.startswith("Всі") \
159
- and not update.startswith("Занадто"):
160
- accumulated = update
161
- else:
162
  accumulated += f"\n\n__{update}__"
 
 
163
  yield accumulated
164
 
165
 
 
1
  """LMAF (Legal Multi-Agent Framework) Gradio chat app.
2
 
3
  Chat interface for multi-agent legal consultation pipeline.
4
+ Runs on prod (agents.legal.org.ua) and HuggingFace Space.
 
5
  """
6
 
7
  from __future__ import annotations
8
 
 
9
  import os
10
  import sys
11
  from pathlib import Path
 
35
  """Run the multi-agent pipeline and yield status updates."""
36
  from lmaf.core.config import Config
37
  from lmaf.engine import LMAF
38
+ from lmaf.state.research_state import CritiqueStatus
39
 
40
  config = Config.from_env()
41
+ lmaf = LMAF(question, config)
42
 
43
  yield "Surveyor: аналізую правовий ландшафт..."
44
+ await lmaf.surveyor.run(lmaf.state)
45
+ lmaf._loop.survey_done = True
46
+ if lmaf.state.survey_summary:
47
+ yield f"**Огляд**: {lmaf.state.survey_summary[:300]}"
48
 
49
  yield "Planner: розробляю стратегію дослідження..."
50
+ await lmaf.planner.run(lmaf.state)
51
+ lmaf._loop.plan_done = True
52
+ if lmaf.state.strategy.approach:
53
+ yield f"**Стратегія**: {lmaf.state.strategy.approach[:300]}"
54
 
55
  for iteration in range(1, config.max_iterations + 1):
56
+ lmaf.state.iteration = iteration
57
 
58
  yield f"Ітерація {iteration}: Orchestrator вирішує наступний крок..."
59
+ orch_result = await lmaf.orchestrator.run(lmaf.state)
60
 
61
  if not orch_result.success:
62
+ if lmaf._loop.consecutive_failures >= config.max_consecutive_failures:
63
  yield "Занадто багато послідовних помилок, зупиняюсь."
64
  break
65
  continue
66
 
67
+ yield "Researcher: шукаю судову практику та законодавство..."
68
+ agent_result = await lmaf.researcher.run(
69
+ lmaf.state, task_description=orch_result.summary
70
  )
71
  if agent_result.success:
72
  yield f"**Знайдено**: {agent_result.summary[:200]}"
73
 
74
  yield "Reviewer: верифікую докази та посилання..."
75
+ await lmaf.reviewer.run(lmaf.state)
76
 
77
  if iteration % config.critic_every_n == 0:
78
  yield "Critic: аудит стратегії та повноти аналізу..."
79
+ critic_result = await lmaf.critic.run(lmaf.state)
80
+ lmaf._loop.last_critic_iteration = iteration
81
 
82
+ for critique in lmaf.state.active_critiques():
83
  if critique.type == "strategy":
84
+ await lmaf.planner.run(
85
+ lmaf.state, revision_critique=critique.details
86
  )
 
87
  critique.status = CritiqueStatus.RESOLVED
88
 
89
  if "можна завершувати: True" in critic_result.summary:
90
  yield "Critic схвалив -- формую консультацію."
91
  break
92
 
93
+ if not lmaf.state.open_questions() and not lmaf.state.active_critiques():
94
  yield "Всі питання вирішено -- формую консультацію."
95
  break
96
 
97
  yield "Formatter: оформлюю фінальну консультацію..."
98
+ await lmaf.formatter.run(lmaf.state)
99
 
100
+ yield lmaf.state.answer or "Не вдалося сформувати відповідь."
101
 
102
 
103
+ async def stream_chat(message: str, history: list[dict]):
104
+ """Async streaming chat handler -- yields incremental updates."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  if not message.strip():
106
  yield "Будь ласка, опишіть вашу правову ситуацію."
107
  return
 
115
  return
116
 
117
  accumulated = ""
118
+ status_prefixes = (
119
+ "Surveyor", "Planner", "Ітерація", "Researcher",
120
+ "Reviewer", "Critic", "Formatter", "Всі", "Занадто",
121
+ )
122
+ async for update in _run_pipeline(message):
123
  if update.startswith("**") or update.startswith("#"):
124
  accumulated += f"\n\n{update}"
125
+ elif update.startswith(status_prefixes):
 
 
 
 
 
 
126
  accumulated += f"\n\n__{update}__"
127
+ else:
128
+ accumulated = update
129
  yield accumulated
130
 
131
 
tests/test_app.py CHANGED
@@ -33,3 +33,9 @@ def test_app_has_iframe_srcdoc():
33
  import app
34
  assert "iframe" in app.ARCHITECTURE_HTML
35
  assert "srcdoc" in app.ARCHITECTURE_HTML
 
 
 
 
 
 
 
33
  import app
34
  assert "iframe" in app.ARCHITECTURE_HTML
35
  assert "srcdoc" in app.ARCHITECTURE_HTML
36
+
37
+
38
+ def test_stream_chat_is_async_generator():
39
+ import app
40
+ import inspect
41
+ assert inspect.isasyncgenfunction(app.stream_chat)