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:
parent
071d74c7a3
commit
60f1c5cb1d
|
|
@ -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)
|
||||||
|
|
@ -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
|
||||||
Loading…
Reference in New Issue