diff --git a/server/python/miroflow_service.py b/server/python/miroflow_service.py new file mode 100644 index 0000000..fe3a889 --- /dev/null +++ b/server/python/miroflow_service.py @@ -0,0 +1,125 @@ +""" +Arcádia MiroFlow Service — Agentes científicos via MiroFlow + Ollama local +FastAPI microserviço porta 8006 +""" +import os +import sys +import time +import uuid +from typing import Optional + +from fastapi import FastAPI, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel + +# Adicionar o submodule MiroFlow ao path +MIROFLOW_MODULE_PATH = os.path.join( + os.path.dirname(__file__), "..", "modules", "miroflow" +) +sys.path.insert(0, os.path.abspath(MIROFLOW_MODULE_PATH)) + +from miroflow.agents.factory import build_agent +from miroflow.agents.context import AgentContext + +OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434") +RESEARCHER_MODEL = os.getenv("MIROFLOW_RESEARCHER_MODEL", "llama3.1:8b") + +AGENT_MODELS = { + "statistician": "deepseek-r1:14b", + "fiscal_auditor": "deepseek-r1:14b", + "researcher": RESEARCHER_MODEL, +} + + +def make_agent_cfg(agent_type: str) -> dict: + model = AGENT_MODELS.get(agent_type, "deepseek-r1:14b") + return { + "type": "IterativeAgentWithToolAndRollback", + "name": f"arcadia_{agent_type}", + "max_turns": 10, + "llm": { + "provider_class": "UnifiedOpenAIClient", + "model_name": model, + "api_key": "ollama", + "base_url": f"{OLLAMA_BASE_URL}/v1", + "max_tokens": 8192, + "temperature": 0.7, + "top_p": 1.0, + "min_p": 0.0, + "top_k": -1, + "reasoning_effort": None, + "repetition_penalty": 1.0, + "max_context_length": -1, + "async_client": True, + "disable_cache_control": True, + "keep_tool_result": -1, + "use_tool_calls": False, + "oai_tool_thinking": False, + }, + } + + +async def run_agent(agent_type: str, task: str) -> tuple[str, str]: + """Retorna (result_text, model_name)""" + if agent_type not in AGENT_MODELS: + raise ValueError(f"Agente desconhecido: {agent_type}. Use: {list(AGENT_MODELS.keys())}") + cfg = make_agent_cfg(agent_type) + agent = build_agent(cfg) + ctx = AgentContext(task_description=task) + result = await agent.run(ctx) + text = result.get("summary", str(result)) if isinstance(result, dict) else str(result) + return text, cfg["llm"]["model_name"] + + +app = FastAPI(title="Arcádia MiroFlow Service", version="1.0.0") +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +class AnalyzeRequest(BaseModel): + agent: str + task: str + context: dict = {} + tenant_id: Optional[int] = None + + +class AnalyzeResponse(BaseModel): + agent: str + model: str + result: str + execution_id: str + duration_ms: int + + +@app.get("/health") +async def health_check(): + return {"status": "ok", "service": "miroflow", "agents": list(AGENT_MODELS.keys())} + + +@app.post("/analyze", response_model=AnalyzeResponse) +async def analyze(req: AnalyzeRequest): + if req.agent not in AGENT_MODELS: + raise HTTPException(status_code=422, detail=f"Agente inválido: {req.agent}") + start = time.time() + try: + result_text, model_name = await run_agent(req.agent, req.task) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + return AnalyzeResponse( + agent=req.agent, + model=model_name, + result=result_text, + execution_id=str(uuid.uuid4()), + duration_ms=int((time.time() - start) * 1000), + ) + + +if __name__ == "__main__": + import uvicorn + port = int(os.environ.get("MIROFLOW_PORT", 8006)) + uvicorn.run(app, host="0.0.0.0", port=port) diff --git a/server/python/test_miroflow_service.py b/server/python/test_miroflow_service.py new file mode 100644 index 0000000..42065bb --- /dev/null +++ b/server/python/test_miroflow_service.py @@ -0,0 +1,53 @@ +"""Tests for miroflow_service.py -- REQ-3.1 to REQ-3.4""" +import sys +import os + +MIROFLOW_MODULE_PATH = os.path.join( + os.path.dirname(__file__), "../modules/miroflow" +) +sys.path.insert(0, os.path.abspath(MIROFLOW_MODULE_PATH)) + +import pytest +from httpx import AsyncClient, ASGITransport +from miroflow_service import app, make_agent_cfg, AnalyzeRequest, AnalyzeResponse + +@pytest.mark.asyncio +async def test_health(): + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as c: + resp = await c.get("/health") + assert resp.status_code == 200 + data = resp.json() + assert data["status"] == "ok" + assert data["service"] == "miroflow" + +def test_statistician_config(): + cfg = make_agent_cfg("statistician") + llm = cfg["llm"] + assert llm["model_name"] == "deepseek-r1:14b" + assert llm["base_url"].endswith("/v1") + +def test_fiscal_auditor_config(): + cfg = make_agent_cfg("fiscal_auditor") + llm = cfg["llm"] + assert llm["model_name"] == "deepseek-r1:14b" + +def test_researcher_config(): + cfg = make_agent_cfg("researcher") + llm = cfg["llm"] + assert llm["model_name"] in ("llama3.1:8b", "llama3.2:3b") + +@pytest.mark.asyncio +async def test_analyze_request_validation(): + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as c: + resp = await c.post("/analyze", json={"agent": "invalid", "task": "test"}) + assert resp.status_code == 422 + +def test_analyze_request_schema(): + req = AnalyzeRequest(agent="statistician", task="Analyze this", context={"k":"v"}, tenant_id=1) + assert req.agent == "statistician" + assert req.task == "Analyze this" + assert req.tenant_id == 1 + resp = AnalyzeResponse(agent="statistician", model="deepseek-r1:14b", result="r", execution_id="abc", duration_ms=500) + assert resp.agent == "statistician" + assert resp.model == "deepseek-r1:14b" + assert resp.duration_ms == 500