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:
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: []
|
||||
Reference in New Issue
Block a user