Update chat interface #70
@@ -6,4 +6,12 @@ def register_friendly_actions(actions: dict[str, str]):
|
|||||||
Aggiunge le descrizioni di un Toolkit al registro globale.
|
Aggiunge le descrizioni di un Toolkit al registro globale.
|
||||||
"""
|
"""
|
||||||
global ACTION_DESCRIPTIONS
|
global ACTION_DESCRIPTIONS
|
||||||
ACTION_DESCRIPTIONS.update(actions)
|
ACTION_DESCRIPTIONS.update(actions)
|
||||||
|
|
||||||
|
def get_user_friendly_action(tool_name: str) -> str:
|
||||||
|
"""
|
||||||
|
Restituisce un messaggio leggibile e descrittivo per l'utente
|
||||||
|
leggendo dal registro globale.
|
||||||
|
"""
|
||||||
|
# Usa il dizionario ACTION_DESCRIPTIONS importato
|
||||||
|
return ACTION_DESCRIPTIONS.get(tool_name, f"⚙️ Eseguo l'operazione: {tool_name}...")
|
||||||
|
|||||||
@@ -155,12 +155,20 @@ class RunMessage:
|
|||||||
self.base_message = f"Running configurations: \n{prefix}{inputs}{suffix}\n\n"
|
self.base_message = f"Running configurations: \n{prefix}{inputs}{suffix}\n\n"
|
||||||
self.emojis = ['🔳', '➡️', '✅']
|
self.emojis = ['🔳', '➡️', '✅']
|
||||||
self.placeholder = '<<<>>>'
|
self.placeholder = '<<<>>>'
|
||||||
|
self.set_steps(["Query Check", "Info Recovery", "Report Generation"])
|
||||||
|
|
||||||
|
def set_steps(self, steps: list[str]) -> 'RunMessage':
|
||||||
|
"""
|
||||||
|
Inizializza gli step di esecuzione con lo stato iniziale.
|
||||||
|
Args:
|
||||||
|
steps (list[str]): Lista degli step da includere nel messaggio.
|
||||||
|
Returns:
|
||||||
|
RunMessage: L'istanza aggiornata di RunMessage.
|
||||||
|
"""
|
||||||
|
self.steps_total = [(f"{self.placeholder} {step}", 0) for step in steps]
|
||||||
|
self.steps_total[0] = (self.steps_total[0][0], 1) # Primo step in esecuzione
|
||||||
self.current = 0
|
self.current = 0
|
||||||
self.steps_total = [
|
return self
|
||||||
(f"{self.placeholder} Query Check", 1),
|
|
||||||
(f"{self.placeholder} Info Recovery", 0),
|
|
||||||
(f"{self.placeholder} Report Generation", 0),
|
|
||||||
]
|
|
||||||
|
|
||||||
def update(self) -> 'RunMessage':
|
def update(self) -> 'RunMessage':
|
||||||
"""
|
"""
|
||||||
@@ -177,15 +185,15 @@ class RunMessage:
|
|||||||
self.steps_total[self.current] = (text_curr, state_curr + 1)
|
self.steps_total[self.current] = (text_curr, state_curr + 1)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def update_step(self, text_extra: str = "") -> 'RunMessage':
|
def update_step_with_tool(self, tool_used: str = "") -> 'RunMessage':
|
||||||
"""
|
"""
|
||||||
Aggiorna il messaggio per lo step corrente.
|
Aggiorna il messaggio per lo step corrente.
|
||||||
Args:
|
Args:
|
||||||
text_extra (str, optional): Testo aggiuntivo da includere nello step. Defaults to "".
|
tool_used (str, optional): Testo aggiuntivo da includere nello step. Defaults to "".
|
||||||
"""
|
"""
|
||||||
text_curr, state_curr = self.steps_total[self.current]
|
text_curr, state_curr = self.steps_total[self.current]
|
||||||
if text_extra:
|
if tool_used:
|
||||||
text_curr = f"{text_curr.replace('╚', '╠')}\n╚═ {text_extra}"
|
text_curr = f"{text_curr.replace('╚', '╠')}\n╚═ {tool_used}"
|
||||||
self.steps_total[self.current] = (text_curr, state_curr)
|
self.steps_total[self.current] = (text_curr, state_curr)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@@ -197,3 +205,4 @@ class RunMessage:
|
|||||||
"""
|
"""
|
||||||
steps = [msg.replace(self.placeholder, self.emojis[state]) for msg, state in self.steps_total]
|
steps = [msg.replace(self.placeholder, self.emojis[state]) for msg, state in self.steps_total]
|
||||||
return self.base_message + "\n".join(steps)
|
return self.base_message + "\n".join(steps)
|
||||||
|
|
||||||
|
|||||||
@@ -1,50 +1,45 @@
|
|||||||
import asyncio
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
from typing import Any, Callable
|
from typing import Any, AsyncGenerator, Callable
|
||||||
from agno.agent import RunEvent
|
from agno.agent import RunEvent
|
||||||
from agno.run.workflow import WorkflowRunEvent
|
from agno.run.workflow import WorkflowRunEvent
|
||||||
from agno.workflow.types import StepInput, StepOutput
|
from agno.workflow.types import StepInput, StepOutput
|
||||||
from agno.workflow.step import Step
|
from agno.workflow.step import Step
|
||||||
from agno.workflow.workflow import Workflow
|
from agno.workflow.workflow import Workflow
|
||||||
|
|
||||||
from app.agents.action_registry import ACTION_DESCRIPTIONS
|
|
||||||
from app.agents.core import *
|
from app.agents.core import *
|
||||||
|
|
||||||
logging = logging.getLogger("pipeline")
|
logging = logging.getLogger("pipeline")
|
||||||
|
|
||||||
|
|
||||||
def _get_user_friendly_action(tool_name: str) -> str:
|
|
||||||
"""
|
|
||||||
Restituisce un messaggio leggibile e descrittivo per l'utente
|
|
||||||
leggendo dal registro globale.
|
|
||||||
"""
|
|
||||||
# Usa il dizionario ACTION_DESCRIPTIONS importato
|
|
||||||
return ACTION_DESCRIPTIONS.get(tool_name, f"⚙️ Eseguo l'operazione: {tool_name}...")
|
|
||||||
|
|
||||||
|
|
||||||
class PipelineEvent(str, Enum):
|
class PipelineEvent(str, Enum):
|
||||||
QUERY_CHECK = "Query Check"
|
QUERY_CHECK = "Query Check"
|
||||||
QUERY_ANALYZER = "Query Analyzer"
|
QUERY_CHECK_END = "Query Check End"
|
||||||
INFO_RECOVERY = "Info Recovery"
|
INFO_RECOVERY = "Info Recovery"
|
||||||
|
INFO_RECOVERY_END = "Info Recovery End"
|
||||||
REPORT_GENERATION = "Report Generation"
|
REPORT_GENERATION = "Report Generation"
|
||||||
REPORT_TRANSLATION = "Report Translation"
|
REPORT_GENERATION_END = "Report Generation End"
|
||||||
RUN_FINISHED = WorkflowRunEvent.workflow_completed.value
|
TOOL_USED = RunEvent.tool_call_started.value
|
||||||
TOOL_USED = RunEvent.tool_call_completed.value
|
TOOL_USED_END = RunEvent.tool_call_completed.value
|
||||||
|
RUN_END = WorkflowRunEvent.workflow_completed.value
|
||||||
|
|
||||||
def check_event(self, event: str, step_name: str) -> bool:
|
def check_event(self, event: str, step_name: str) -> bool:
|
||||||
return event == self.value or (WorkflowRunEvent.step_completed == event and step_name == self.value)
|
if event == self.value:
|
||||||
|
return True
|
||||||
|
|
||||||
|
index = self.value.rfind(" End")
|
||||||
|
value = self.value[:index] if index > -1 else self.value
|
||||||
|
step_state = WorkflowRunEvent.step_completed if index > -1 else WorkflowRunEvent.step_started
|
||||||
|
return step_name == value and step_state == event
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_log_events(cls, run_id: int) -> list[tuple['PipelineEvent', Callable[[Any], None]]]:
|
def get_log_events(cls, run_id: int) -> list[tuple['PipelineEvent', Callable[[Any], str | None]]]:
|
||||||
return [
|
return [
|
||||||
(PipelineEvent.QUERY_CHECK, lambda _: logging.info(f"[{run_id}] Query Check completed.")),
|
(PipelineEvent.QUERY_CHECK_END, lambda _: logging.info(f"[{run_id}] Query Check completed.")),
|
||||||
(PipelineEvent.QUERY_ANALYZER, lambda _: logging.info(f"[{run_id}] Query Analyzer completed.")),
|
(PipelineEvent.INFO_RECOVERY_END, lambda _: logging.info(f"[{run_id}] Info Recovery completed.")),
|
||||||
(PipelineEvent.INFO_RECOVERY, lambda _: logging.info(f"[{run_id}] Info Recovery completed.")),
|
(PipelineEvent.REPORT_GENERATION_END, lambda _: logging.info(f"[{run_id}] Report Generation completed.")),
|
||||||
(PipelineEvent.REPORT_GENERATION, lambda _: logging.info(f"[{run_id}] Report Generation completed.")),
|
(PipelineEvent.TOOL_USED_END, lambda e: logging.info(f"[{run_id}] Tool used [{e.tool.tool_name} {e.tool.tool_args}] by {e.agent_name}.")),
|
||||||
(PipelineEvent.TOOL_USED, lambda e: logging.info(f"[{run_id}] Tool used [{e.tool.tool_name} {e.tool.tool_args}] by {e.agent_name}.")),
|
(PipelineEvent.RUN_END, lambda _: logging.info(f"[{run_id}] Run completed.")),
|
||||||
(PipelineEvent.RUN_FINISHED, lambda _: logging.info(f"[{run_id}] Run completed.")),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -63,7 +58,7 @@ class Pipeline:
|
|||||||
"""
|
"""
|
||||||
self.inputs = inputs
|
self.inputs = inputs
|
||||||
|
|
||||||
def interact(self, listeners: list[tuple[PipelineEvent, Callable[[Any], None]]] = []) -> str:
|
async def interact(self, listeners: list[tuple[PipelineEvent, Callable[[Any], str | None]]] = []) -> str:
|
||||||
"""
|
"""
|
||||||
Esegue la pipeline di agenti per rispondere alla query dell'utente.
|
Esegue la pipeline di agenti per rispondere alla query dell'utente.
|
||||||
Args:
|
Args:
|
||||||
@@ -71,9 +66,12 @@ class Pipeline:
|
|||||||
Returns:
|
Returns:
|
||||||
La risposta generata dalla pipeline.
|
La risposta generata dalla pipeline.
|
||||||
"""
|
"""
|
||||||
return asyncio.run(self.interact_async(listeners))
|
response = ""
|
||||||
|
async for chunk in self.interact_stream(listeners):
|
||||||
|
response = chunk
|
||||||
|
return response
|
||||||
|
|
||||||
async def interact_async(self, listeners: list[tuple[PipelineEvent, Callable[[Any], None]]] = []) -> str:
|
async def interact_stream(self, listeners: list[tuple[PipelineEvent, Callable[[Any], str | None]]] = []) -> AsyncGenerator[str, None]:
|
||||||
"""
|
"""
|
||||||
Versione asincrona che esegue la pipeline di agenti per rispondere alla query dell'utente.
|
Versione asincrona che esegue la pipeline di agenti per rispondere alla query dell'utente.
|
||||||
Args:
|
Args:
|
||||||
@@ -91,26 +89,6 @@ class Pipeline:
|
|||||||
)
|
)
|
||||||
|
|
||||||
workflow = self.build_workflow()
|
workflow = self.build_workflow()
|
||||||
result = await self.run(workflow, query, events=events)
|
|
||||||
return result
|
|
||||||
|
|
||||||
async def interact_stream(self, listeners: list[tuple[PipelineEvent, Callable[[Any], None]]] = []):
|
|
||||||
"""
|
|
||||||
Versione asincrona in streaming che ESEGUE (yield) la pipeline,
|
|
||||||
restituendo gli aggiornamenti di stato e il risultato finale.
|
|
||||||
"""
|
|
||||||
run_id = random.randint(1000, 9999) # Per tracciare i log
|
|
||||||
logging.info(f"[{run_id}] Pipeline query: {self.inputs.user_query}")
|
|
||||||
|
|
||||||
events = [*PipelineEvent.get_log_events(run_id), *listeners]
|
|
||||||
query = QueryInputs(
|
|
||||||
user_query=self.inputs.user_query,
|
|
||||||
strategy=self.inputs.strategy.description
|
|
||||||
)
|
|
||||||
|
|
||||||
workflow = self.build_workflow()
|
|
||||||
|
|
||||||
# Delega al classmethod 'run_stream' per lo streaming
|
|
||||||
async for item in self.run_stream(workflow, query, events=events):
|
async for item in self.run_stream(workflow, query, events=events):
|
||||||
yield item
|
yield item
|
||||||
|
|
||||||
|
|
|||||||
@@ -143,69 +121,37 @@ class Pipeline:
|
|||||||
])
|
])
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def run(cls, workflow: Workflow, query: QueryInputs,
|
async def run_stream(cls, workflow: Workflow, query: QueryInputs, events: list[tuple[PipelineEvent, Callable[[Any], str | None]]]) -> AsyncGenerator[str, None]:
|
||||||
events: list[tuple[PipelineEvent, Callable[[Any], None]]]) -> str:
|
|
||||||
"""
|
|
||||||
Esegue il workflow e gestisce gli eventi, restituendo solo il risultato finale.
|
|
||||||
Consuma il generatore 'run_stream'.
|
|
||||||
"""
|
|
||||||
final_result = "Errore durante l'esecuzione del workflow."
|
|
||||||
# Consuma il generatore e salva solo l'ultimo item
|
|
||||||
async for item in cls.run_stream(workflow, query, events):
|
|
||||||
final_result = item
|
|
||||||
|
|
||||||
return final_result
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def run_stream(cls, workflow: Workflow, query: QueryInputs,
|
|
||||||
events: list[tuple[PipelineEvent, Callable[[Any], None]]]):
|
|
||||||
"""
|
"""
|
||||||
Esegue il workflow e restituisce gli eventi di stato e il risultato finale.
|
Esegue il workflow e restituisce gli eventi di stato e il risultato finale.
|
||||||
|
Args:
|
||||||
|
workflow: L'istanza di Workflow da eseguire.
|
||||||
|
query: Gli input della query.
|
||||||
|
events: La lista di eventi e callback da gestire durante l'esecuzione.
|
||||||
|
Yields:
|
||||||
|
Aggiornamenti di stato e la risposta finale generata dal workflow.
|
||||||
"""
|
"""
|
||||||
iterator = await workflow.arun(query, stream=True, stream_intermediate_steps=True)
|
iterator = await workflow.arun(query, stream=True, stream_intermediate_steps=True)
|
||||||
content = None
|
content = None
|
||||||
current_active_step = None
|
|
||||||
|
|
||||||
async for event in iterator:
|
async for event in iterator:
|
||||||
step_name = getattr(event, 'step_name', '')
|
step_name = getattr(event, 'step_name', '')
|
||||||
|
|
||||||
# 1. Chiama i listeners (per i log)
|
# Chiama i listeners (se presenti) per ogni evento
|
||||||
for app_event, listener in events:
|
for app_event, listener in events:
|
||||||
if app_event.check_event(event.event, step_name):
|
if app_event.check_event(event.event, step_name):
|
||||||
listener(event)
|
update = listener(event)
|
||||||
|
The ellipsis '...' at the end of the string is inconsistent with other yield statements. Line 196 has three dots while lines 181, 183, 185, and 194 use '...' (ellipsis character). Consider using the ellipsis character for consistency. The ellipsis '...' at the end of the string is inconsistent with other yield statements. Line 196 has three dots while lines 181, 183, 185, and 194 use '...' (ellipsis character). Consider using the ellipsis character for consistency.
```suggestion
yield f"Sto usando uno strumento sconosciuto…"
```
[nitpick] The default error message 'Errore durante l'esecuzione del workflow.' may not be reached in practice since the stream should always yield at least one item (the error message from line 213). This initialization could be misleading. Consider using a more descriptive default or documenting why this fallback exists. [nitpick] The default error message 'Errore durante l'esecuzione del workflow.' may not be reached in practice since the stream should always yield at least one item (the error message from line 213). This initialization could be misleading. Consider using a more descriptive default or documenting why this fallback exists.
```suggestion
# Fallback: if the workflow yields no results, return a descriptive error.
final_result = "[Pipeline Error] Nessun risultato prodotto dal workflow. (Fallback: run_stream non ha generato output)"
```
|
|||||||
|
if update: yield update
|
||||||
|
|
||||||
# 2. Restituisce gli aggiornamenti di stato per Gradio
|
# Salva il contenuto finale quando uno step è completato
|
||||||
if event.event == WorkflowRunEvent.step_started.value:
|
if event.event == WorkflowRunEvent.step_completed.value:
|
||||||
current_active_step = step_name
|
|
||||||
if step_name == PipelineEvent.QUERY_CHECK.value:
|
|
||||||
yield "🔍 Sto controllando la tua richiesta..."
|
|
||||||
elif step_name == PipelineEvent.INFO_RECOVERY.value:
|
|
||||||
yield "📊 Sto recuperando i dati (mercato, news, social)..."
|
|
||||||
elif step_name == PipelineEvent.REPORT_GENERATION.value:
|
|
||||||
yield "✍️ Sto scrivendo il report finale..."
|
|
||||||
|
|
||||||
# Gestisce gli eventi di tool promossi dal Team
|
|
||||||
elif event.event == PipelineEvent.TOOL_USED.value:
|
|
||||||
if current_active_step == PipelineEvent.INFO_RECOVERY.value:
|
|
||||||
tool_object = getattr(event, 'tool', None)
|
|
||||||
if tool_object:
|
|
||||||
tool_name = getattr(tool_object, 'tool_name', 'uno strumento sconosciuto')
|
|
||||||
user_message = _get_user_friendly_action(tool_name)
|
|
||||||
yield f"{user_message}"
|
|
||||||
else:
|
|
||||||
yield f"Sto usando uno strumento sconosciuto..."
|
|
||||||
|
|
||||||
# 3. Salva il contenuto finale quando uno step è completato
|
|
||||||
elif event.event == WorkflowRunEvent.step_completed.value:
|
|
||||||
current_active_step = None
|
|
||||||
content = getattr(event, 'content', '')
|
content = getattr(event, 'content', '')
|
||||||
|
|
||||||
# 4. Restituisce la risposta finale
|
# Restituisce la risposta finale
|
||||||
if content and isinstance(content, str):
|
if content and isinstance(content, str):
|
||||||
think_str = "</think>"
|
think_str = "</think>"
|
||||||
think = content.rfind(think_str)
|
think = content.rfind(think_str)
|
||||||
final_answer = content[(think + len(think_str)):] if think != -1 else content
|
yield content[(think + len(think_str)):] if think != -1 else content
|
||||||
yield final_answer
|
|
||||||
elif content and isinstance(content, QueryOutputs):
|
elif content and isinstance(content, QueryOutputs):
|
||||||
yield content.response
|
yield content.response
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
from typing import Any, Callable
|
||||||
import gradio as gr
|
import gradio as gr
|
||||||
from app.agents.pipeline import Pipeline, PipelineInputs
|
from app.agents.action_registry import get_user_friendly_action
|
||||||
|
from app.agents.pipeline import Pipeline, PipelineEvent, PipelineInputs
|
||||||
|
|
||||||
|
|
||||||
class ChatManager:
|
class ChatManager:
|
||||||
@@ -56,10 +58,15 @@ class ChatManager:
|
|||||||
"""
|
"""
|
||||||
self.inputs.user_query = message
|
self.inputs.user_query = message
|
||||||
pipeline = Pipeline(self.inputs)
|
pipeline = Pipeline(self.inputs)
|
||||||
|
listeners: list[tuple[PipelineEvent, Callable[[Any], str | None]]] = [
|
||||||
|
(PipelineEvent.QUERY_CHECK, lambda _: "🔍 Sto controllando la tua richiesta..."),
|
||||||
|
(PipelineEvent.INFO_RECOVERY, lambda _: "📊 Sto recuperando i dati (mercato, news, social)..."),
|
||||||
|
(PipelineEvent.REPORT_GENERATION, lambda _: "✍️ Sto scrivendo il report finale..."),
|
||||||
|
(PipelineEvent.TOOL_USED, lambda e: get_user_friendly_action(e.tool.tool_name))
|
||||||
|
]
|
||||||
|
|
||||||
response = None
|
response = None
|
||||||
# Itera sul nuovo generatore asincrono
|
async for chunk in pipeline.interact_stream(listeners=listeners):
|
||||||
async for chunk in pipeline.interact_stream():
|
|
||||||
response = chunk # Salva l'ultimo chunk (che sarà la risposta finale)
|
response = chunk # Salva l'ultimo chunk (che sarà la risposta finale)
|
||||||
yield response # Restituisce l'aggiornamento (o la risposta finale) a Gradio
|
yield response # Restituisce l'aggiornamento (o la risposta finale) a Gradio
|
||||||
|
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ class TelegramApp:
|
|||||||
await bot.delete_message(chat_id=chat_id, message_id=update.message.id)
|
await bot.delete_message(chat_id=chat_id, message_id=update.message.id)
|
||||||
|
|
||||||
def update_user(update_step: str = "") -> None:
|
def update_user(update_step: str = "") -> None:
|
||||||
if update_step: run_message.update_step(update_step)
|
if update_step: run_message.update_step_with_tool(update_step)
|
||||||
else: run_message.update()
|
else: run_message.update()
|
||||||
|
|
||||||
message = run_message.get_latest()
|
message = run_message.get_latest()
|
||||||
@@ -280,11 +280,11 @@ class TelegramApp:
|
|||||||
|
|
||||||
await bot.send_chat_action(chat_id=chat_id, action=ChatAction.TYPING)
|
await bot.send_chat_action(chat_id=chat_id, action=ChatAction.TYPING)
|
||||||
pipeline = Pipeline(inputs)
|
pipeline = Pipeline(inputs)
|
||||||
report_content = await pipeline.interact_async(listeners=[
|
report_content = await pipeline.interact(listeners=[
|
||||||
(PipelineEvent.QUERY_CHECK, lambda _: update_user()),
|
(PipelineEvent.QUERY_CHECK_END, lambda _: update_user()),
|
||||||
(PipelineEvent.TOOL_USED, lambda e: update_user(e.tool.tool_name.replace('get_', '').replace("_", "\\_"))),
|
(PipelineEvent.TOOL_USED_END, lambda e: update_user(e.tool.tool_name.replace('get_', '').replace("_", "\\_"))),
|
||||||
(PipelineEvent.INFO_RECOVERY, lambda _: update_user()),
|
(PipelineEvent.INFO_RECOVERY_END, lambda _: update_user()),
|
||||||
(PipelineEvent.REPORT_GENERATION, lambda _: update_user()),
|
(PipelineEvent.REPORT_GENERATION_END, lambda _: update_user()),
|
||||||
])
|
])
|
||||||
|
|
||||||
# attach report file to the message
|
# attach report file to the message
|
||||||
|
|||||||
Reference in New Issue
Block a user
[nitpick] The comment describes the function as 'ESEGUE (yield)' which is inconsistent with the style used in similar comments. The word 'ESEGUE' (executes) appears to be emphasized but doesn't align well with the yield concept. Consider clarifying that it 'yields' or 'streams' intermediate results and the final response.