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