File size: 7,386 Bytes
fea1bd1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# main_agent.py
# -*- coding: utf-8 -*-
"""
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

# ----------------------------------------------------------------------
# 1. CONFIGURAÇÃO INICIAL E DEPENDÊNCIAS
# ----------------------------------------------------------------------
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))

# --- Importações do Projeto ---
from autonomous_config import config, Config
from agent_tools import TOOL_REGISTRY, get_tools_description_for_llm
# >>> NOVO: Importa as estruturas de dados do seu próprio arquivo <<<
from agent_models import Task, ToolCall

# ----------------------------------------------------------------------
# 2. CLASSES DO AGENTE (Sem as definições de dataclass)
# ----------------------------------------------------------------------

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()
            # Fallback para uma conversa simples se o planejamento falhar
            # (Em uma versão futura, isso também poderia ser uma chamada ao LLM)
            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.")

# ----------------------------------------------------------------------
# 3. HOOKS DO CHAINLIT (A Camada de Interface)
# ----------------------------------------------------------------------
@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()