|
|
|
|
|
|
|
|
""" |
|
|
HASHIRU 6.8 - ARQUITETURA FINAL ESTÁVEL |
|
|
|
|
|
Esta versão finaliza a separação de responsabilidades, movendo os |
|
|
modelos de dados para seu próprio arquivo e estabilizando a arquitetura. |
|
|
""" |
|
|
from __future__ import annotations |
|
|
import traceback |
|
|
import sys |
|
|
import pathlib |
|
|
import logging |
|
|
import json |
|
|
|
|
|
import chainlit as cl |
|
|
import httpx |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s - [%(levelname)s] - %(message)s") |
|
|
logger = logging.getLogger("HASHIRU_AGENT") |
|
|
|
|
|
ROOT_PATH = pathlib.Path(__file__).resolve().parent |
|
|
if str(ROOT_PATH) not in sys.path: |
|
|
sys.path.insert(0, str(ROOT_PATH)) |
|
|
|
|
|
|
|
|
from autonomous_config import config, Config |
|
|
from agent_tools import TOOL_REGISTRY, get_tools_description_for_llm |
|
|
|
|
|
from agent_models import Task, ToolCall |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Planner: |
|
|
"""Cria um plano de ação (uma lista de ToolCalls) para resolver a solicitação.""" |
|
|
def __init__(self, config: Config, http_client: httpx.AsyncClient): |
|
|
self.config = config |
|
|
self.http_client = http_client |
|
|
self.system_prompt = ( |
|
|
"Você é um planejador para um agente de IA. Sua tarefa é decompor a solicitação do usuário em uma sequência de passos, onde cada passo é uma chamada a uma ferramenta. " |
|
|
"As ferramentas disponíveis são:\n{tools}\n\n" |
|
|
"Responda APENAS com um objeto JSON contendo uma chave 'plan', que é uma lista de ações. " |
|
|
"Cada ação na lista deve ter 'tool_name' e 'tool_input'. Exemplo de resposta: " |
|
|
'{"plan": [{"tool_name": "read_file", "tool_input": {"file_path": "main.py"}}]}' |
|
|
) |
|
|
|
|
|
async def create_task_with_plan(self, user_input: str) -> Task: |
|
|
"""Usa o LLM para criar uma Task completa com um plano de ferramentas.""" |
|
|
model = self.config.get_model("reasoning") |
|
|
prompt = self.system_prompt.format(tools=get_tools_description_for_llm()) |
|
|
|
|
|
try: |
|
|
response_json = await self._call_ollama(model, f"Crie um plano para: '{user_input}'", prompt) |
|
|
data = json.loads(response_json) |
|
|
plan_data = data.get("plan", []) |
|
|
plan = [ToolCall(**call) for call in plan_data] |
|
|
return Task("tool_based_plan", "Executar plano de ferramentas", user_input, plan) |
|
|
except (json.JSONDecodeError, httpx.RequestError, TypeError) as e: |
|
|
logger.error(f"Falha ao criar plano, tratando como conversa. Erro: {e}", exc_info=True) |
|
|
return Task("general_conversation", "Conversa geral", user_input, []) |
|
|
|
|
|
async def _call_ollama(self, model: str, prompt: str, system: str) -> str: |
|
|
payload = {"model": model, "prompt": prompt, "system": system, "stream": False, "format": "json"} |
|
|
r = await self.http_client.post(f"{self.config.ollama.base_url}/api/generate", json=payload) |
|
|
r.raise_for_status() |
|
|
return r.json().get("response", "{}") |
|
|
|
|
|
class Executor: |
|
|
"""Executa um plano de ToolCalls em sequência.""" |
|
|
def __init__(self, config: Config): |
|
|
self.config = config |
|
|
|
|
|
async def execute_plan(self, plan: list[ToolCall]) -> str: |
|
|
"""Itera sobre um plano e executa cada chamada de ferramenta.""" |
|
|
if not plan: |
|
|
return "## ✅ Plano Concluído\nNenhuma ação foi planejada, pois a tarefa não exigia ferramentas." |
|
|
|
|
|
results = [] |
|
|
for i, tool_call in enumerate(plan): |
|
|
async with cl.Step(name=f"Passo {i+1}: `{tool_call.tool_name}`") as step: |
|
|
step.input = str(tool_call.tool_input) |
|
|
|
|
|
tool = TOOL_REGISTRY.get(tool_call.tool_name) |
|
|
if not tool: |
|
|
output = f"Erro: Ferramenta '{tool_call.tool_name}' não encontrada." |
|
|
else: |
|
|
try: |
|
|
output = tool.execute(tool_call.tool_input) |
|
|
except Exception as e: |
|
|
output = f"Erro ao executar a ferramenta '{tool.name}': {e}" |
|
|
|
|
|
step.output = output |
|
|
results.append(f"#### ✅ Passo {i+1}: `{tool.name}`\n**Resultado:**\n```\n{output}\n```") |
|
|
|
|
|
return "## 📝 Relatório Final de Execução\n\n" + "\n\n---\n\n".join(results) |
|
|
|
|
|
class AgentSession: |
|
|
"""Gerencia o ciclo de vida completo de uma interação.""" |
|
|
def __init__(self, config: Config): |
|
|
self.config = config |
|
|
self.http_client = httpx.AsyncClient(timeout=config.ollama.timeout) |
|
|
self.planner = Planner(config, self.http_client) |
|
|
self.executor = Executor(config) |
|
|
|
|
|
async def handle_message(self, user_input: str): |
|
|
"""Orquestra o fluxo de Planejar -> Executar.""" |
|
|
await cl.Message(content=self.config.processing_message).send() |
|
|
|
|
|
task = await self.planner.create_task_with_plan(user_input) |
|
|
|
|
|
if not task.plan: |
|
|
plan_summary = "Não foi criado um plano de ferramentas. Tentando responder diretamente..." |
|
|
await cl.Message(content=plan_summary).send() |
|
|
|
|
|
|
|
|
response = "Desculpe, não consegui transformar sua solicitação em um plano de ação. Pode tentar ser mais específico ou pedir algo que use as ferramentas disponíveis?" |
|
|
else: |
|
|
plan_summary = "**Plano de Ação:**\n" + "\n".join([f"- Usar `{tc.tool_name}`" for tc in task.plan]) |
|
|
await cl.Message(content=plan_summary).send() |
|
|
|
|
|
await cl.Message(content=self.config.executing_message).send() |
|
|
response = await self.executor.execute_plan(task.plan) |
|
|
|
|
|
await cl.Message(content=response).send() |
|
|
|
|
|
async def cleanup(self): |
|
|
await self.http_client.aclose() |
|
|
logger.info("Sessão do agente finalizada.") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@cl.on_chat_start |
|
|
async def on_chat_start(): |
|
|
agent_session = AgentSession(config) |
|
|
cl.user_session.set("agent_session", agent_session) |
|
|
await cl.Message(content=config.startup_banner).send() |
|
|
|
|
|
@cl.on_message |
|
|
async def on_message(message: cl.Message): |
|
|
agent = cl.user_session.get("agent_session") |
|
|
user_input = (message.content or "").strip() |
|
|
if not user_input or not agent: return |
|
|
|
|
|
try: |
|
|
await agent.handle_message(user_input) |
|
|
except Exception: |
|
|
tb = traceback.format_exc() |
|
|
logger.error(f"Erro irrecuperável no fluxo principal:\n{tb}") |
|
|
await cl.Message(content=f"💥 **Erro Inesperado no Sistema:**\n```\n{tb}\n```").send() |
|
|
|
|
|
@cl.on_chat_end |
|
|
async def on_chat_end(): |
|
|
if agent := cl.user_session.get("agent_session"): |
|
|
await agent.cleanup() |