feat(03-01): criar miroflow_service.py FastAPI porta 8006 com 3 agentes + testes pytest

- FastAPI microservico porta 8006 com agentes statistician, fiscal_auditor, researcher
- Configuracao de modelos: deepseek-r1:14b (stat/fiscal), llama3.1:8b com fallback 3b (researcher)
- OLLAMA_BASE_URL/v1 via UnifiedOpenAIClient
- test_miroflow_service.py: 6 testes cobrindo REQ-3.1 a REQ-3.4 (todos passam)
- sys.path.insert para submodule MiroFlow sem modificar o submodulo
This commit is contained in:
Jonas Pacheco 2026-03-25 10:50:05 -03:00
parent 071d74c7a3
commit 60f1c5cb1d
2 changed files with 178 additions and 0 deletions

View File

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

View File

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