Update chat interface #70

Merged
trojanhorse47 merged 9 commits from 47-update-chat-interface into main 2025-10-31 14:24:39 +01:00
5 changed files with 83 additions and 113 deletions
Showing only changes of commit 2803263bed - Show all commits

View File

@@ -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}...")

View File

@@ -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)

View File

@@ -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
copilot-pull-request-reviewer[bot] commented 2025-10-30 20:22:23 +01:00 (Migrated from github.com)
Review

[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.

        Versione asincrona in streaming che restituisce (yield) gli aggiornamenti di stato intermedi
        e il risultato finale della pipeline.
[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. ```suggestion Versione asincrona in streaming che restituisce (yield) gli aggiornamenti di stato intermedi e il risultato finale della pipeline. ```
@@ -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)
copilot-pull-request-reviewer[bot] commented 2025-10-30 20:22:22 +01:00 (Migrated from github.com)
Review

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.

                        yield f"Sto usando uno strumento sconosciuto…"
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…" ```
copilot-pull-request-reviewer[bot] commented 2025-10-30 20:22:23 +01:00 (Migrated from github.com)
Review

[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.

        # 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)"
[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:

View File

@@ -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

View File

@@ -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