mirror of
https://github.com/hoshikawa2/agent_platform_oci.git
synced 2026-07-09 14:04:19 +00:00
Ajustes na documentação e remanejamento dos folders
This commit is contained in:
121
.idea/workspace.xml
generated
Normal file
121
.idea/workspace.xml
generated
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AutoImportSettings">
|
||||||
|
<option name="autoReloadType" value="SELECTIVE" />
|
||||||
|
</component>
|
||||||
|
<component name="ChangeListManager">
|
||||||
|
<list default="true" id="30a0e1d8-9d7d-469b-b241-f300911cee8a" name="Changes" comment="">
|
||||||
|
<change beforePath="$PROJECT_DIR$/.env.example" beforeDir="false" afterPath="$PROJECT_DIR$/.env.example" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/.idea/.gitignore" beforeDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/.idea/agent_platform_oci.iml" beforeDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/.idea/codeStyles/Project.xml" beforeDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/.idea/codeStyles/codeStyleConfig.xml" beforeDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/.idea/modules.xml" beforeDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/.idea/vcs.xml" beforeDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/README_en.md" beforeDir="false" afterPath="$PROJECT_DIR$/README_en.md" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/apps/agent_gateway/config/backends.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/apps/agent_gateway/config/backends.yaml" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/apps/ai_gateway/Dockerfile" beforeDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/apps/ai_gateway/README.md" beforeDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/apps/ai_gateway/app/__init__.py" beforeDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/apps/ai_gateway/app/main.py" beforeDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/apps/ai_gateway/requirements.txt" beforeDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/apps/mcp_gateway/Dockerfile" beforeDir="false" afterPath="$PROJECT_DIR$/apps/mcp_gateway/Dockerfile" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/apps/mcp_gateway/app/__init__.py" beforeDir="false" afterPath="$PROJECT_DIR$/apps/mcp_gateway/app/__init__.py" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/apps/mcp_gateway/app/main.py" beforeDir="false" afterPath="$PROJECT_DIR$/apps/mcp_gateway/app/main.py" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/apps/mcp_gateway/requirements.txt" beforeDir="false" afterPath="$PROJECT_DIR$/apps/mcp_gateway/requirements.txt" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/deploy/k8s/mcp-gateway.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/deploy/k8s/mcp-gateway.yaml" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/docs/MODULAR_REMAP.md" beforeDir="false" afterPath="$PROJECT_DIR$/docs/MODULAR_REMAP.md" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/libs/agent_framework/config/mcp_servers.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/libs/agent_framework/config/mcp_servers.yaml" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/libs/agent_framework/src/agent_framework/config/settings.py" beforeDir="false" afterPath="$PROJECT_DIR$/libs/agent_framework/src/agent_framework/config/settings.py" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/libs/agent_framework/src/agent_framework/mcp/tool_router.py" beforeDir="false" afterPath="$PROJECT_DIR$/libs/agent_framework/src/agent_framework/mcp/tool_router.py" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/pyproject.toml" beforeDir="false" afterPath="$PROJECT_DIR$/pyproject.toml" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/scripts/run_mcp_gateway.sh" beforeDir="false" afterPath="$PROJECT_DIR$/scripts/run_mcp_gateway.sh" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/specs/SPEC-001-Architecture.md" beforeDir="false" afterPath="$PROJECT_DIR$/specs/SPEC-001-Architecture.md" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/specs/SPEC-008-Deployment.md" beforeDir="false" afterPath="$PROJECT_DIR$/specs/SPEC-008-Deployment.md" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/specs/SPEC-010-Agent-Development.md" beforeDir="false" afterPath="$PROJECT_DIR$/specs/SPEC-010-Agent-Development.md" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/specs/SPEC-014-Templates-and-Agent-Creation-Model.md" beforeDir="false" afterPath="$PROJECT_DIR$/specs/SPEC-014-Templates-and-Agent-Creation-Model.md" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/templates/agent_template_backend/.env" beforeDir="false" afterPath="$PROJECT_DIR$/templates/agent_template_backend/.env" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/templates/agent_template_backend_day_zero/.env" beforeDir="false" afterPath="$PROJECT_DIR$/templates/agent_template_backend_day_zero/.env" afterDir="false" />
|
||||||
|
</list>
|
||||||
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
|
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||||
|
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||||
|
</component>
|
||||||
|
<component name="ComposerSettings">
|
||||||
|
<execution />
|
||||||
|
</component>
|
||||||
|
<component name="Git.Settings">
|
||||||
|
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||||
|
</component>
|
||||||
|
<component name="GitHubPullRequestSearchHistory">{
|
||||||
|
"lastFilter": {
|
||||||
|
"state": "OPEN",
|
||||||
|
"assignee": "hoshikawa2"
|
||||||
|
}
|
||||||
|
}</component>
|
||||||
|
<component name="GithubPullRequestsUISettings">{
|
||||||
|
"selectedUrlAndAccountId": {
|
||||||
|
"url": "https://github.com/hoshikawa2/agent_platform_oci.git",
|
||||||
|
"accountId": "bd799ac0-7624-42b2-999a-b96e6e11f98a"
|
||||||
|
}
|
||||||
|
}</component>
|
||||||
|
<component name="KubernetesApiPersistence"><![CDATA[{}]]></component>
|
||||||
|
<component name="KubernetesApiProvider"><![CDATA[{
|
||||||
|
"isMigrated": true
|
||||||
|
}]]></component>
|
||||||
|
<component name="PhpWorkspaceProjectConfiguration" interpreter_name="PHP 8.1" />
|
||||||
|
<component name="ProjectCodeStyleSettingsMigration">
|
||||||
|
<option name="version" value="2" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectColorInfo">{
|
||||||
|
"customColor": "",
|
||||||
|
"associatedIndex": 2
|
||||||
|
}</component>
|
||||||
|
<component name="ProjectId" id="3FQ8m21arbKaZKgqpTYDVjVGTBf" />
|
||||||
|
<component name="ProjectViewState">
|
||||||
|
<option name="hideEmptyMiddlePackages" value="true" />
|
||||||
|
<option name="showLibraryContents" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PropertiesComponent">{
|
||||||
|
"keyToString": {
|
||||||
|
"ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
||||||
|
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||||
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||||
|
"RunOnceActivity.git.unshallow": "true",
|
||||||
|
"SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
||||||
|
"git-widget-placeholder": "main",
|
||||||
|
"node.js.detected.package.eslint": "true",
|
||||||
|
"node.js.detected.package.tslint": "true",
|
||||||
|
"node.js.selected.package.eslint": "(autodetect)",
|
||||||
|
"node.js.selected.package.tslint": "(autodetect)",
|
||||||
|
"nodejs_package_manager_path": "npm",
|
||||||
|
"settings.editor.selected.configurable": "configurable.group.appearance",
|
||||||
|
"vue.rearranger.settings.migration": "true"
|
||||||
|
}
|
||||||
|
}</component>
|
||||||
|
<component name="SharedIndexes">
|
||||||
|
<attachedChunks>
|
||||||
|
<set>
|
||||||
|
<option value="bundled-jdk-9823dce3aa75-fbdcb00ec9e3-intellij.indexing.shared.core-IU-251.23774.435" />
|
||||||
|
<option value="bundled-js-predefined-d6986cc7102b-f27c65a3e318-JavaScript-IU-251.23774.435" />
|
||||||
|
</set>
|
||||||
|
</attachedChunks>
|
||||||
|
</component>
|
||||||
|
<component name="TaskManager">
|
||||||
|
<task active="true" id="Default" summary="Default task">
|
||||||
|
<changelist id="30a0e1d8-9d7d-469b-b241-f300911cee8a" name="Changes" comment="" />
|
||||||
|
<created>1781996088468</created>
|
||||||
|
<option name="number" value="Default" />
|
||||||
|
<option name="presentableId" value="Default" />
|
||||||
|
<updated>1781996088468</updated>
|
||||||
|
<workItem from="1781996091031" duration="1242000" />
|
||||||
|
<workItem from="1782038604623" duration="6404000" />
|
||||||
|
</task>
|
||||||
|
<servers />
|
||||||
|
</component>
|
||||||
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
|
<option name="version" value="3" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
275
MANUAL_AGENT_PLATFORM_GATEWAYS.md
Normal file
275
MANUAL_AGENT_PLATFORM_GATEWAYS.md
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
# Agent Platform OCI — Manual Oficial de Agent Gateway e MCP Gateway
|
||||||
|
|
||||||
|
## Objetivo
|
||||||
|
|
||||||
|
Este documento consolida:
|
||||||
|
- Arquitetura oficial
|
||||||
|
- Inventário dos componentes
|
||||||
|
- Procedimento completo de execução local
|
||||||
|
- MCP Gateway
|
||||||
|
- Agent Gateway
|
||||||
|
- Backend Runtime
|
||||||
|
- Frontend
|
||||||
|
- Testes E2E
|
||||||
|
- Troubleshooting
|
||||||
|
- Decisões arquiteturais
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Arquitetura Oficial
|
||||||
|
|
||||||
|
Frontend (5173)
|
||||||
|
↓
|
||||||
|
Agent Gateway (9000)
|
||||||
|
↓
|
||||||
|
Agent Template Backend / Runtime (8000)
|
||||||
|
↓
|
||||||
|
MCP Gateway (8300)
|
||||||
|
↓
|
||||||
|
Telecom MCP Server (8100)
|
||||||
|
Retail MCP Server (8200)
|
||||||
|
|
||||||
|
Não existe AI Gateway.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Portas Oficiais
|
||||||
|
|
||||||
|
| Componente | Porta |
|
||||||
|
|------------|--------|
|
||||||
|
| Frontend | 5173 |
|
||||||
|
| Agent Gateway | 9000 |
|
||||||
|
| Backend Runtime | 8000 |
|
||||||
|
| MCP Gateway | 8300 |
|
||||||
|
| Telecom MCP Server | 8100 |
|
||||||
|
| Retail MCP Server | 8200 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Variáveis Oficiais
|
||||||
|
|
||||||
|
## Agent Template Backend
|
||||||
|
|
||||||
|
ENABLE_MCP_TOOLS=true
|
||||||
|
|
||||||
|
MCP_GATEWAY_ENABLED=true
|
||||||
|
MCP_GATEWAY_URL=http://localhost:8300
|
||||||
|
MCP_GATEWAY_TIMEOUT_SECONDS=60
|
||||||
|
MCP_GATEWAY_AGENT_ID=telecom_contas
|
||||||
|
MCP_GATEWAY_TENANT_ID=default
|
||||||
|
|
||||||
|
## Agent Gateway
|
||||||
|
|
||||||
|
DEFAULT_AGENT_BACKEND_URL=http://localhost:8000
|
||||||
|
AGENT_GATEWAY_GOVERNANCE_CONFIG=config/gateway_governance.yaml
|
||||||
|
|
||||||
|
## MCP Gateway
|
||||||
|
|
||||||
|
MCP_GATEWAY_CONFIG_PATH=config/mcp_gateway.yaml
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Ordem de Inicialização
|
||||||
|
|
||||||
|
1. Telecom MCP Server
|
||||||
|
2. Retail MCP Server
|
||||||
|
3. MCP Gateway
|
||||||
|
4. Agent Template Backend
|
||||||
|
5. Agent Gateway
|
||||||
|
6. Frontend
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Terminal 1 — Telecom MCP Server
|
||||||
|
|
||||||
|
cd mcp/servers/telecom_mcp_server
|
||||||
|
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
python -m uvicorn main:app --host 0.0.0.0 --port 8100 --reload
|
||||||
|
|
||||||
|
Validação:
|
||||||
|
|
||||||
|
curl http://localhost:8100/health
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Terminal 2 — Retail MCP Server
|
||||||
|
|
||||||
|
cd mcp/servers/retail_mcp_server
|
||||||
|
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
python -m uvicorn main:app --host 0.0.0.0 --port 8200 --reload
|
||||||
|
|
||||||
|
Validação:
|
||||||
|
|
||||||
|
curl http://localhost:8200/health
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Terminal 3 — MCP Gateway
|
||||||
|
|
||||||
|
cd apps/mcp_gateway
|
||||||
|
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
export MCP_GATEWAY_CONFIG_PATH=config/mcp_gateway.yaml
|
||||||
|
|
||||||
|
python -m uvicorn app.main:app --host 0.0.0.0 --port 8300 --reload
|
||||||
|
|
||||||
|
Validações:
|
||||||
|
|
||||||
|
curl http://localhost:8300/health
|
||||||
|
curl http://localhost:8300/ready
|
||||||
|
curl http://localhost:8300/v1/tools
|
||||||
|
|
||||||
|
Teste:
|
||||||
|
|
||||||
|
curl -X POST http://localhost:8300/v1/tools/consultar_fatura/invoke
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Terminal 4 — Agent Template Backend
|
||||||
|
|
||||||
|
cd templates/agent_template_backend
|
||||||
|
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||||
|
|
||||||
|
Validações:
|
||||||
|
|
||||||
|
curl http://localhost:8000/health
|
||||||
|
curl http://localhost:8000/agents
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Terminal 5 — Agent Gateway
|
||||||
|
|
||||||
|
cd apps/agent_gateway
|
||||||
|
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
export DEFAULT_AGENT_BACKEND_URL=http://localhost:8000
|
||||||
|
export AGENT_GATEWAY_GOVERNANCE_CONFIG=config/gateway_governance.yaml
|
||||||
|
|
||||||
|
python -m uvicorn app.main:app --host 0.0.0.0 --port 9000 --reload
|
||||||
|
|
||||||
|
Validações:
|
||||||
|
|
||||||
|
curl http://localhost:9000/health
|
||||||
|
|
||||||
|
Teste:
|
||||||
|
|
||||||
|
curl -X POST http://localhost:9000/gateway/message
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Terminal 6 — Frontend
|
||||||
|
|
||||||
|
cd agent_frontend
|
||||||
|
|
||||||
|
npm install
|
||||||
|
|
||||||
|
npm run dev -- --host 0.0.0.0 --port 5173
|
||||||
|
|
||||||
|
Abrir:
|
||||||
|
|
||||||
|
http://localhost:5173
|
||||||
|
|
||||||
|
Backend URL:
|
||||||
|
|
||||||
|
http://localhost:9000
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Fluxo de Tools
|
||||||
|
|
||||||
|
Agent
|
||||||
|
↓
|
||||||
|
MCPToolRouter
|
||||||
|
↓
|
||||||
|
MCPGatewayClient
|
||||||
|
↓
|
||||||
|
MCP Gateway
|
||||||
|
↓
|
||||||
|
MCP Server
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Teste Integrado E2E
|
||||||
|
|
||||||
|
Frontend
|
||||||
|
↓
|
||||||
|
Agent Gateway
|
||||||
|
↓
|
||||||
|
Backend Runtime
|
||||||
|
↓
|
||||||
|
MCP Gateway
|
||||||
|
↓
|
||||||
|
Telecom MCP Server
|
||||||
|
|
||||||
|
Resultado esperado:
|
||||||
|
|
||||||
|
- Agent Gateway recebe requisição
|
||||||
|
- Runtime executa LangGraph
|
||||||
|
- MCP Gateway resolve tool
|
||||||
|
- MCP Server responde
|
||||||
|
- Usuário recebe resposta
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Troubleshooting
|
||||||
|
|
||||||
|
## Backend chamando MCP Server direto
|
||||||
|
|
||||||
|
Confirmar:
|
||||||
|
|
||||||
|
MCP_GATEWAY_ENABLED=true
|
||||||
|
|
||||||
|
MCP_GATEWAY_URL=http://localhost:8300
|
||||||
|
|
||||||
|
## Porta incorreta
|
||||||
|
|
||||||
|
A porta oficial do MCP Gateway é:
|
||||||
|
|
||||||
|
8300
|
||||||
|
|
||||||
|
## Agent Gateway não encontra Backend
|
||||||
|
|
||||||
|
Validar:
|
||||||
|
|
||||||
|
curl http://localhost:8000/health
|
||||||
|
|
||||||
|
## MCP Gateway não encontra MCP Server
|
||||||
|
|
||||||
|
Validar:
|
||||||
|
|
||||||
|
curl http://localhost:8100/health
|
||||||
|
curl http://localhost:8200/health
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Decisões Arquiteturais Oficiais
|
||||||
|
|
||||||
|
- Não existe AI Gateway
|
||||||
|
- Agent Gateway centraliza governança
|
||||||
|
- Runtime executa LangGraph
|
||||||
|
- Runtime executa LLM
|
||||||
|
- MCP Gateway centraliza tools
|
||||||
|
- MCP Servers executam tools
|
||||||
|
- Backend usa MCP Gateway
|
||||||
|
- gateway_runtime.env.example foi removido
|
||||||
|
- MCP_GATEWAY_* fica no .env do backend
|
||||||
|
- Porta oficial MCP Gateway = 8300
|
||||||
14
apps/agent_gateway/app/config/governance_loader.py
Normal file
14
apps/agent_gateway/app/config/governance_loader.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
def load_gateway_governance_config(path: str | None = None) -> dict[str, Any]:
|
||||||
|
config_path = Path(path or os.getenv("AGENT_GATEWAY_GOVERNANCE_CONFIG", "config/gateway_governance.yaml"))
|
||||||
|
if not config_path.exists():
|
||||||
|
return {}
|
||||||
|
return yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
|
||||||
1
apps/agent_gateway/app/governance/__init__.py
Normal file
1
apps/agent_gateway/app/governance/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
14
apps/agent_gateway/app/governance/audit.py
Normal file
14
apps/agent_gateway/app/governance/audit.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
logger = logging.getLogger("agent_gateway.governance")
|
||||||
|
|
||||||
|
|
||||||
|
def audit_event(name: str, payload: dict[str, Any]) -> None:
|
||||||
|
safe = dict(payload)
|
||||||
|
if "message" in safe:
|
||||||
|
safe["message_len"] = len(str(safe.pop("message") or ""))
|
||||||
|
logger.info("%s %s", name, json.dumps(safe, ensure_ascii=False, default=str))
|
||||||
11
apps/agent_gateway/app/governance/evaluation_hooks.py
Normal file
11
apps/agent_gateway/app/governance/evaluation_hooks.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
class EvaluationHooks:
|
||||||
|
def before_backend_call(self, request_payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
return request_payload
|
||||||
|
|
||||||
|
def after_backend_call(self, response_payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
return response_payload
|
||||||
66
apps/agent_gateway/app/governance/model_policies.py
Normal file
66
apps/agent_gateway/app/governance/model_policies.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
class ModelPolicyError(RuntimeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ModelPolicyResolver:
|
||||||
|
def __init__(self, config: dict[str, Any]):
|
||||||
|
self.config = config or {}
|
||||||
|
|
||||||
|
def resolve_profile(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
tenant_id: str,
|
||||||
|
agent_id: str | None,
|
||||||
|
operation: str,
|
||||||
|
requested_profile: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
profiles = self.config.get("profiles", {}) or {}
|
||||||
|
operation_profiles = self.config.get("operation_profiles", {}) or {}
|
||||||
|
|
||||||
|
profile_name = requested_profile or operation_profiles.get(operation) or "default"
|
||||||
|
profile = profiles.get(profile_name)
|
||||||
|
if not profile:
|
||||||
|
raise ModelPolicyError(f"Model profile not found: {profile_name}")
|
||||||
|
|
||||||
|
self._validate_policy(
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
agent_id=agent_id,
|
||||||
|
profile_name=profile_name,
|
||||||
|
profile=profile,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"profile": profile_name,
|
||||||
|
"provider": profile.get("provider"),
|
||||||
|
"model": profile.get("model"),
|
||||||
|
"parameters": {
|
||||||
|
k: v for k, v in profile.items()
|
||||||
|
if k not in {"provider", "model"}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def _validate_policy(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
tenant_id: str,
|
||||||
|
agent_id: str | None,
|
||||||
|
profile_name: str,
|
||||||
|
profile: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
policies = self.config.get("policies", {}) or {}
|
||||||
|
tenant_policies = policies.get("tenants", {}) or {}
|
||||||
|
tenant_policy = tenant_policies.get(tenant_id) or tenant_policies.get("default") or {}
|
||||||
|
|
||||||
|
allowed_profiles = tenant_policy.get("allowed_profiles")
|
||||||
|
if allowed_profiles and profile_name not in allowed_profiles:
|
||||||
|
raise ModelPolicyError(f"Profile not allowed for tenant={tenant_id}: {profile_name}")
|
||||||
|
|
||||||
|
allowed_providers = tenant_policy.get("allowed_providers")
|
||||||
|
provider = profile.get("provider")
|
||||||
|
if allowed_providers and provider not in allowed_providers:
|
||||||
|
raise ModelPolicyError(f"Provider not allowed for tenant={tenant_id}: {provider}")
|
||||||
35
apps/agent_gateway/app/governance/rate_limit.py
Normal file
35
apps/agent_gateway/app/governance/rate_limit.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import time
|
||||||
|
from collections import defaultdict, deque
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
class RateLimitExceeded(RuntimeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InMemoryRateLimiter:
|
||||||
|
def __init__(self, config: dict[str, Any]):
|
||||||
|
self.config = config or {}
|
||||||
|
self.events: dict[str, deque[float]] = defaultdict(deque)
|
||||||
|
|
||||||
|
def check(self, *, tenant_id: str, agent_id: str | None, channel: str | None) -> None:
|
||||||
|
default_limit = ((self.config.get("default") or {}).get("requests_per_minute")) or 600
|
||||||
|
agent_limits = self.config.get("agents") or {}
|
||||||
|
channel_limits = self.config.get("channels") or {}
|
||||||
|
|
||||||
|
limit = default_limit
|
||||||
|
if agent_id and agent_id in agent_limits:
|
||||||
|
limit = agent_limits[agent_id].get("requests_per_minute", limit)
|
||||||
|
if channel and channel in channel_limits:
|
||||||
|
limit = min(limit, channel_limits[channel].get("requests_per_minute", limit))
|
||||||
|
|
||||||
|
key = f"{tenant_id}:{agent_id or '*'}:{channel or '*'}"
|
||||||
|
now = time.time()
|
||||||
|
bucket = self.events[key]
|
||||||
|
while bucket and bucket[0] < now - 60:
|
||||||
|
bucket.popleft()
|
||||||
|
if len(bucket) >= int(limit):
|
||||||
|
raise RateLimitExceeded(f"Gateway rate limit exceeded for {key}: {limit}/min")
|
||||||
|
bucket.append(now)
|
||||||
14
apps/agent_gateway/app/governance/usage.py
Normal file
14
apps/agent_gateway/app/governance/usage.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
class UsageRecorder:
|
||||||
|
def record_gateway_request(self, payload: dict[str, Any]) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def record_model_policy(self, payload: dict[str, Any]) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def record_backend_response(self, payload: dict[str, Any]) -> None:
|
||||||
|
return None
|
||||||
78
apps/agent_gateway/app/governance_middleware.py
Normal file
78
apps/agent_gateway/app/governance_middleware.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from fastapi import HTTPException
|
||||||
|
|
||||||
|
from app.config.governance_loader import load_gateway_governance_config
|
||||||
|
from app.governance.audit import audit_event
|
||||||
|
from app.governance.evaluation_hooks import EvaluationHooks
|
||||||
|
from app.governance.model_policies import ModelPolicyError, ModelPolicyResolver
|
||||||
|
from app.governance.rate_limit import InMemoryRateLimiter, RateLimitExceeded
|
||||||
|
from app.governance.usage import UsageRecorder
|
||||||
|
|
||||||
|
|
||||||
|
class AgentGatewayGovernance:
|
||||||
|
def __init__(self, config: dict[str, Any] | None = None):
|
||||||
|
self.config = config if config is not None else load_gateway_governance_config()
|
||||||
|
self.model_resolver = ModelPolicyResolver((self.config.get("model_governance") or {}))
|
||||||
|
self.rate_limiter = InMemoryRateLimiter((self.config.get("rate_limits") or {}))
|
||||||
|
self.usage = UsageRecorder()
|
||||||
|
self.eval_hooks = EvaluationHooks()
|
||||||
|
|
||||||
|
def prepare_backend_request(self, gateway_request: dict[str, Any]) -> tuple[dict[str, Any], dict[str, str]]:
|
||||||
|
tenant_id = gateway_request.get("tenant_id") or "default"
|
||||||
|
agent_id = gateway_request.get("agent_id")
|
||||||
|
channel = gateway_request.get("channel")
|
||||||
|
payload = gateway_request.get("payload") or {}
|
||||||
|
metadata = payload.setdefault("metadata", {})
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.rate_limiter.check(tenant_id=tenant_id, agent_id=agent_id, channel=channel)
|
||||||
|
|
||||||
|
model_policy = self.model_resolver.resolve_profile(
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
agent_id=agent_id,
|
||||||
|
operation=metadata.get("operation") or "agent.final_answer",
|
||||||
|
requested_profile=metadata.get("llm_profile"),
|
||||||
|
)
|
||||||
|
metadata["model_policy"] = model_policy
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"X-Agent-Gateway-Governance": "enabled",
|
||||||
|
"X-Model-Profile": str(model_policy.get("profile") or ""),
|
||||||
|
"X-Model-Provider": str(model_policy.get("provider") or ""),
|
||||||
|
"X-Model-Name": str(model_policy.get("model") or ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
audit_event("agent_gateway.request.governed", {
|
||||||
|
"tenant_id": tenant_id,
|
||||||
|
"agent_id": agent_id,
|
||||||
|
"channel": channel,
|
||||||
|
"model_policy": model_policy,
|
||||||
|
"request_id": metadata.get("request_id"),
|
||||||
|
"message": payload.get("message"),
|
||||||
|
})
|
||||||
|
|
||||||
|
self.usage.record_gateway_request({
|
||||||
|
"tenant_id": tenant_id,
|
||||||
|
"agent_id": agent_id,
|
||||||
|
"channel": channel,
|
||||||
|
"metadata": metadata,
|
||||||
|
})
|
||||||
|
|
||||||
|
governed = self.eval_hooks.before_backend_call(gateway_request)
|
||||||
|
return governed, headers
|
||||||
|
|
||||||
|
except RateLimitExceeded as exc:
|
||||||
|
raise HTTPException(status_code=429, detail=str(exc)) from exc
|
||||||
|
except ModelPolicyError as exc:
|
||||||
|
raise HTTPException(status_code=403, detail=str(exc)) from exc
|
||||||
|
|
||||||
|
def process_backend_response(self, response_payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
response_payload = self.eval_hooks.after_backend_call(response_payload)
|
||||||
|
self.usage.record_backend_response(response_payload)
|
||||||
|
audit_event("agent_gateway.response.completed", {
|
||||||
|
"metadata": response_payload.get("metadata") if isinstance(response_payload, dict) else {},
|
||||||
|
})
|
||||||
|
return response_payload
|
||||||
40
apps/agent_gateway/app/routes/governed_proxy_example.py
Normal file
40
apps/agent_gateway/app/routes/governed_proxy_example.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from fastapi import APIRouter, HTTPException, Request
|
||||||
|
|
||||||
|
from app.governance_middleware import AgentGatewayGovernance
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
governance = AgentGatewayGovernance()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/gateway/message/governed")
|
||||||
|
async def governed_gateway_message(request: Request):
|
||||||
|
"""Example governed proxy route.
|
||||||
|
|
||||||
|
Use as reference to patch the existing /gateway/message handler.
|
||||||
|
"""
|
||||||
|
|
||||||
|
body: dict[str, Any] = await request.json()
|
||||||
|
backend_url = os.getenv("DEFAULT_AGENT_BACKEND_URL", "http://localhost:8000")
|
||||||
|
|
||||||
|
governed_body, headers = governance.prepare_backend_request(body)
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=90) as client:
|
||||||
|
resp = await client.post(
|
||||||
|
f"{backend_url.rstrip('/')}/gateway/message",
|
||||||
|
json=governed_body,
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
data = resp.json()
|
||||||
|
return governance.process_backend_response(data)
|
||||||
|
except httpx.HTTPStatusError as exc:
|
||||||
|
raise HTTPException(status_code=exc.response.status_code, detail=exc.response.text) from exc
|
||||||
|
except Exception as exc:
|
||||||
|
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
||||||
56
apps/agent_gateway/config/gateway_governance.yaml
Normal file
56
apps/agent_gateway/config/gateway_governance.yaml
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
model_governance:
|
||||||
|
profiles:
|
||||||
|
default:
|
||||||
|
provider: oci_openai
|
||||||
|
model: openai.gpt-4.1
|
||||||
|
temperature: 0.2
|
||||||
|
max_tokens: 2048
|
||||||
|
|
||||||
|
router:
|
||||||
|
provider: oci_openai
|
||||||
|
model: openai.gpt-4.1
|
||||||
|
temperature: 0
|
||||||
|
max_tokens: 500
|
||||||
|
|
||||||
|
judge:
|
||||||
|
provider: oci_openai
|
||||||
|
model: openai.gpt-4.1
|
||||||
|
temperature: 0
|
||||||
|
max_tokens: 800
|
||||||
|
|
||||||
|
operation_profiles:
|
||||||
|
router.intent: router
|
||||||
|
agent.final_answer: default
|
||||||
|
judge.response_quality: judge
|
||||||
|
|
||||||
|
policies:
|
||||||
|
tenants:
|
||||||
|
default:
|
||||||
|
allowed_providers:
|
||||||
|
- oci_openai
|
||||||
|
- oci_sdk
|
||||||
|
- mock
|
||||||
|
allowed_profiles:
|
||||||
|
- default
|
||||||
|
- router
|
||||||
|
- judge
|
||||||
|
|
||||||
|
rate_limits:
|
||||||
|
default:
|
||||||
|
requests_per_minute: 600
|
||||||
|
channels:
|
||||||
|
whatsapp:
|
||||||
|
requests_per_minute: 120
|
||||||
|
web:
|
||||||
|
requests_per_minute: 300
|
||||||
|
agents:
|
||||||
|
telecom_contas:
|
||||||
|
requests_per_minute: 180
|
||||||
|
|
||||||
|
backend_headers:
|
||||||
|
propagate_model_policy: true
|
||||||
|
propagate_trace_context: true
|
||||||
|
|
||||||
|
evaluation:
|
||||||
|
enabled: true
|
||||||
|
sample_rate: 1.0
|
||||||
3
apps/mcp_gateway/.env.example
Normal file
3
apps/mcp_gateway/.env.example
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
MCP_GATEWAY_CONFIG_PATH=config/mcp_gateway.yaml
|
||||||
|
MCP_GATEWAY_HOST=0.0.0.0
|
||||||
|
MCP_GATEWAY_PORT=8300
|
||||||
172
apps/mcp_gateway/config/mcp_gateway.yaml
Normal file
172
apps/mcp_gateway/config/mcp_gateway.yaml
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
# Dedicated MCP Gateway configuration.
|
||||||
|
# The agent backend/framework calls this gateway; this gateway calls the final MCP servers.
|
||||||
|
|
||||||
|
servers:
|
||||||
|
telecom:
|
||||||
|
enabled: true
|
||||||
|
protocol: legacy_http
|
||||||
|
transport: http
|
||||||
|
# Local run: uvicorn mcp.servers.telecom_mcp_server.main:app --port 8100
|
||||||
|
url: http://localhost:8100/mcp
|
||||||
|
timeout_seconds: 30
|
||||||
|
|
||||||
|
retail:
|
||||||
|
enabled: true
|
||||||
|
protocol: legacy_http
|
||||||
|
transport: http
|
||||||
|
# Local run: uvicorn mcp.servers.retail_mcp_server.main:app --port 8200
|
||||||
|
url: http://localhost:8200/mcp
|
||||||
|
timeout_seconds: 30
|
||||||
|
|
||||||
|
tools:
|
||||||
|
consultar_fatura:
|
||||||
|
version: 1.0.0
|
||||||
|
server: telecom
|
||||||
|
endpoint: /tools/call
|
||||||
|
protocol: legacy_http
|
||||||
|
enabled: true
|
||||||
|
idempotent: true
|
||||||
|
cache_ttl_seconds: 300
|
||||||
|
timeout_seconds: 30
|
||||||
|
retry: {enabled: true, max_attempts: 2, backoff_ms: 250}
|
||||||
|
allowed_agents: []
|
||||||
|
allowed_channels: []
|
||||||
|
required_business_keys: []
|
||||||
|
|
||||||
|
consultar_pagamentos:
|
||||||
|
version: 1.0.0
|
||||||
|
server: telecom
|
||||||
|
endpoint: /tools/call
|
||||||
|
protocol: legacy_http
|
||||||
|
enabled: true
|
||||||
|
idempotent: true
|
||||||
|
cache_ttl_seconds: 300
|
||||||
|
timeout_seconds: 30
|
||||||
|
retry: {enabled: true, max_attempts: 2, backoff_ms: 250}
|
||||||
|
allowed_agents: []
|
||||||
|
allowed_channels: []
|
||||||
|
required_business_keys: []
|
||||||
|
|
||||||
|
consultar_plano:
|
||||||
|
version: 1.0.0
|
||||||
|
server: telecom
|
||||||
|
endpoint: /tools/call
|
||||||
|
protocol: legacy_http
|
||||||
|
enabled: true
|
||||||
|
idempotent: true
|
||||||
|
cache_ttl_seconds: 300
|
||||||
|
timeout_seconds: 30
|
||||||
|
retry: {enabled: true, max_attempts: 2, backoff_ms: 250}
|
||||||
|
allowed_agents: []
|
||||||
|
allowed_channels: []
|
||||||
|
required_business_keys: []
|
||||||
|
|
||||||
|
listar_servicos:
|
||||||
|
version: 1.0.0
|
||||||
|
server: telecom
|
||||||
|
endpoint: /tools/call
|
||||||
|
protocol: legacy_http
|
||||||
|
enabled: true
|
||||||
|
idempotent: true
|
||||||
|
cache_ttl_seconds: 300
|
||||||
|
timeout_seconds: 30
|
||||||
|
retry: {enabled: true, max_attempts: 2, backoff_ms: 250}
|
||||||
|
allowed_agents: []
|
||||||
|
allowed_channels: []
|
||||||
|
required_business_keys: []
|
||||||
|
|
||||||
|
consultar_pedido:
|
||||||
|
version: 1.0.0
|
||||||
|
server: retail
|
||||||
|
endpoint: /tools/call
|
||||||
|
protocol: legacy_http
|
||||||
|
enabled: true
|
||||||
|
idempotent: true
|
||||||
|
cache_ttl_seconds: 300
|
||||||
|
timeout_seconds: 30
|
||||||
|
retry: {enabled: true, max_attempts: 2, backoff_ms: 250}
|
||||||
|
allowed_agents: []
|
||||||
|
allowed_channels: []
|
||||||
|
required_business_keys: []
|
||||||
|
|
||||||
|
consultar_entrega:
|
||||||
|
version: 1.0.0
|
||||||
|
server: retail
|
||||||
|
endpoint: /tools/call
|
||||||
|
protocol: legacy_http
|
||||||
|
enabled: true
|
||||||
|
idempotent: true
|
||||||
|
cache_ttl_seconds: 300
|
||||||
|
timeout_seconds: 30
|
||||||
|
retry: {enabled: true, max_attempts: 2, backoff_ms: 250}
|
||||||
|
allowed_agents: []
|
||||||
|
allowed_channels: []
|
||||||
|
required_business_keys: []
|
||||||
|
|
||||||
|
solicitar_troca:
|
||||||
|
version: 1.0.0
|
||||||
|
server: retail
|
||||||
|
endpoint: /tools/call
|
||||||
|
protocol: legacy_http
|
||||||
|
enabled: true
|
||||||
|
idempotent: false
|
||||||
|
cache_ttl_seconds: 0
|
||||||
|
timeout_seconds: 30
|
||||||
|
retry: {enabled: false}
|
||||||
|
allowed_agents: []
|
||||||
|
allowed_channels: []
|
||||||
|
required_business_keys: []
|
||||||
|
|
||||||
|
solicitar_devolucao:
|
||||||
|
version: 1.0.0
|
||||||
|
server: retail
|
||||||
|
endpoint: /tools/call
|
||||||
|
protocol: legacy_http
|
||||||
|
enabled: true
|
||||||
|
idempotent: false
|
||||||
|
cache_ttl_seconds: 0
|
||||||
|
timeout_seconds: 30
|
||||||
|
retry: {enabled: false}
|
||||||
|
allowed_agents: []
|
||||||
|
allowed_channels: []
|
||||||
|
required_business_keys: []
|
||||||
|
|
||||||
|
# Optional mapping if the backend sends canonical BusinessContext directly to the gateway.
|
||||||
|
# In the normal framework path, the framework already maps before calling the gateway.
|
||||||
|
parameter_mapping:
|
||||||
|
consultar_fatura:
|
||||||
|
customer_key: msisdn
|
||||||
|
contract_key: invoice_id
|
||||||
|
interaction_key: ura_call_id
|
||||||
|
session_key: session_id
|
||||||
|
consultar_pagamentos:
|
||||||
|
customer_key: msisdn
|
||||||
|
interaction_key: ura_call_id
|
||||||
|
session_key: session_id
|
||||||
|
consultar_plano:
|
||||||
|
customer_key: msisdn
|
||||||
|
resource_key: asset_id
|
||||||
|
contract_key: asset_id
|
||||||
|
session_key: session_id
|
||||||
|
listar_servicos:
|
||||||
|
customer_key: msisdn
|
||||||
|
session_key: session_id
|
||||||
|
consultar_pedido:
|
||||||
|
customer_key: customer_id
|
||||||
|
contract_key: order_id
|
||||||
|
session_key: session_id
|
||||||
|
consultar_entrega:
|
||||||
|
contract_key: order_id
|
||||||
|
session_key: session_id
|
||||||
|
solicitar_troca:
|
||||||
|
contract_key: order_id
|
||||||
|
session_key: session_id
|
||||||
|
solicitar_devolucao:
|
||||||
|
contract_key: order_id
|
||||||
|
session_key: session_id
|
||||||
|
|
||||||
|
auth:
|
||||||
|
enabled: false
|
||||||
|
static_tokens:
|
||||||
|
runtime-local-token:
|
||||||
|
agents: []
|
||||||
20
deploy/docker/docker-compose.mcp-gateway.yml
Normal file
20
deploy/docker/docker-compose.mcp-gateway.yml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
services:
|
||||||
|
mcp_gateway:
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: apps/mcp_gateway/Dockerfile
|
||||||
|
ports:
|
||||||
|
- "8300:8300"
|
||||||
|
depends_on:
|
||||||
|
- mock_telecom_mcp
|
||||||
|
|
||||||
|
mock_telecom_mcp:
|
||||||
|
image: python:3.12-slim
|
||||||
|
working_dir: /app
|
||||||
|
command: >
|
||||||
|
sh -c "pip install -r requirements.txt &&
|
||||||
|
uvicorn app:app --host 0.0.0.0 --port 8001"
|
||||||
|
volumes:
|
||||||
|
- ../../mcp/servers/mock_telecom_mcp:/app
|
||||||
|
ports:
|
||||||
|
- "8001:8001"
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def get_gateway_model_policy(state: dict[str, Any]) -> dict[str, Any] | None:
|
||||||
|
metadata = state.get("metadata") or {}
|
||||||
|
policy = metadata.get("model_policy")
|
||||||
|
return policy if isinstance(policy, dict) else None
|
||||||
|
|
||||||
|
|
||||||
|
def apply_gateway_model_policy_to_llm_kwargs(
|
||||||
|
state: dict[str, Any],
|
||||||
|
fallback_profile: dict[str, Any] | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
policy = get_gateway_model_policy(state)
|
||||||
|
if not policy:
|
||||||
|
return fallback_profile or {}
|
||||||
|
|
||||||
|
params = dict(policy.get("parameters") or {})
|
||||||
|
if policy.get("model"):
|
||||||
|
params["model"] = policy["model"]
|
||||||
|
if policy.get("provider"):
|
||||||
|
params["provider"] = policy["provider"]
|
||||||
|
if policy.get("profile"):
|
||||||
|
params["profile"] = policy["profile"]
|
||||||
|
return params
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from .mcp_gateway_client import MCPGatewayClient
|
||||||
|
|
||||||
|
__all__ = ["MCPGatewayClient"]
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
|
||||||
|
class MCPGatewayClient:
|
||||||
|
def __init__(self, base_url: str, token: str | None = None, timeout_seconds: int = 60):
|
||||||
|
self.base_url = base_url.rstrip("/")
|
||||||
|
self.token = token
|
||||||
|
self.timeout_seconds = timeout_seconds
|
||||||
|
|
||||||
|
def _headers(self) -> dict[str, str]:
|
||||||
|
return {"Authorization": f"Bearer {self.token}"} if self.token else {}
|
||||||
|
|
||||||
|
async def list_tools(self) -> dict[str, Any]:
|
||||||
|
async with httpx.AsyncClient(timeout=self.timeout_seconds) as client:
|
||||||
|
response = await client.get(f"{self.base_url}/v1/tools", headers=self._headers())
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
async def invoke_tool(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
tenant_id: str,
|
||||||
|
agent_id: str,
|
||||||
|
channel: str | None,
|
||||||
|
tool_name: str,
|
||||||
|
arguments: dict[str, Any] | None = None,
|
||||||
|
business_context: dict[str, Any] | None = None,
|
||||||
|
metadata: dict[str, Any] | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
payload = {
|
||||||
|
"tenant_id": tenant_id,
|
||||||
|
"agent_id": agent_id,
|
||||||
|
"channel": channel,
|
||||||
|
"tool_name": tool_name,
|
||||||
|
"arguments": arguments or {},
|
||||||
|
"business_context": business_context or {},
|
||||||
|
"metadata": metadata or {},
|
||||||
|
}
|
||||||
|
async with httpx.AsyncClient(timeout=self.timeout_seconds) as client:
|
||||||
|
response = await client.post(
|
||||||
|
f"{self.base_url}/v1/tools/{tool_name}/invoke",
|
||||||
|
json=payload,
|
||||||
|
headers=self._headers(),
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from agent_framework.gateways import MCPGatewayClient
|
||||||
|
|
||||||
|
|
||||||
|
class MCPGatewayRuntimeMixin:
|
||||||
|
mcp_gateway_client: MCPGatewayClient | None = None
|
||||||
|
|
||||||
|
async def _invoke_mcp_gateway_tool(
|
||||||
|
self,
|
||||||
|
state: dict[str, Any],
|
||||||
|
tool_name: str,
|
||||||
|
arguments: dict[str, Any] | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
if not self.mcp_gateway_client:
|
||||||
|
raise RuntimeError("MCP Gateway client not configured")
|
||||||
|
|
||||||
|
result = await self.mcp_gateway_client.invoke_tool(
|
||||||
|
tenant_id=state.get("tenant_id", "default"),
|
||||||
|
agent_id=state.get("agent_id") or state.get("route") or "unknown",
|
||||||
|
channel=state.get("channel"),
|
||||||
|
tool_name=tool_name,
|
||||||
|
arguments=arguments or {},
|
||||||
|
business_context=state.get("business_context") or {},
|
||||||
|
metadata={
|
||||||
|
"session_id": state.get("session_id"),
|
||||||
|
"conversation_key": state.get("conversation_key"),
|
||||||
|
"trace_id": (state.get("metadata") or {}).get("trace_id"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
state.setdefault("mcp_results", []).append(result)
|
||||||
|
return result
|
||||||
29
mcp/servers/mock_telecom_mcp/app.py
Normal file
29
mcp/servers/mock_telecom_mcp/app.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
app = FastAPI(title="Mock Telecom MCP Server")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
async def health():
|
||||||
|
return {"status": "ok", "service": "mock_telecom_mcp"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/tools/consultar_fatura")
|
||||||
|
async def consultar_fatura(payload: dict):
|
||||||
|
return {
|
||||||
|
"invoice_id": payload.get("invoice_id") or "INV-001",
|
||||||
|
"msisdn": payload.get("msisdn"),
|
||||||
|
"valor_total": 249.90,
|
||||||
|
"vencimento": "2026-06-10",
|
||||||
|
"status": "ABERTA",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/tools/consultar_pagamentos")
|
||||||
|
async def consultar_pagamentos(payload: dict):
|
||||||
|
return {
|
||||||
|
"msisdn": payload.get("msisdn"),
|
||||||
|
"pagamentos": [
|
||||||
|
{"data": "2026-06-05", "valor": 249.90, "status": "CONFIRMADO"}
|
||||||
|
],
|
||||||
|
}
|
||||||
2
mcp/servers/mock_telecom_mcp/requirements.txt
Normal file
2
mcp/servers/mock_telecom_mcp/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fastapi>=0.110
|
||||||
|
uvicorn[standard]>=0.27
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from agent_framework.gateways import MCPGatewayClient
|
||||||
|
|
||||||
|
|
||||||
|
def build_mcp_gateway_client() -> MCPGatewayClient | None:
|
||||||
|
if os.getenv("MCP_GATEWAY_ENABLED", "true").lower() != "true":
|
||||||
|
return None
|
||||||
|
|
||||||
|
return MCPGatewayClient(
|
||||||
|
base_url=os.getenv("MCP_GATEWAY_URL", "http://localhost:8300"),
|
||||||
|
token=os.getenv("MCP_GATEWAY_TOKEN") or None,
|
||||||
|
timeout_seconds=int(os.getenv("MCP_GATEWAY_TIMEOUT_SECONDS", "60")),
|
||||||
|
)
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from agent_framework.gateways import MCPGatewayClient
|
||||||
|
|
||||||
|
|
||||||
|
def build_mcp_gateway_client() -> MCPGatewayClient | None:
|
||||||
|
if os.getenv("MCP_GATEWAY_ENABLED", "true").lower() != "true":
|
||||||
|
return None
|
||||||
|
|
||||||
|
return MCPGatewayClient(
|
||||||
|
base_url=os.getenv("MCP_GATEWAY_URL", "http://localhost:8300"),
|
||||||
|
token=os.getenv("MCP_GATEWAY_TOKEN") or None,
|
||||||
|
timeout_seconds=int(os.getenv("MCP_GATEWAY_TIMEOUT_SECONDS", "60")),
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user