@qubiit/lmagent 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +18 -0
- package/AGENTS.md +169 -0
- package/CLAUDE.md +122 -0
- package/CONTRIBUTING.md +90 -0
- package/LICENSE +21 -0
- package/README.md +195 -0
- package/config/commands.yaml +194 -0
- package/config/levels.yaml +135 -0
- package/config/models.yaml +192 -0
- package/config/settings.yaml +405 -0
- package/config/tools-extended.yaml +534 -0
- package/config/tools.yaml +437 -0
- package/docs/assets/logo.png +0 -0
- package/docs/commands.md +132 -0
- package/docs/customization-guide.md +445 -0
- package/docs/getting-started.md +154 -0
- package/docs/how-to-start.md +242 -0
- package/docs/navigation-index.md +227 -0
- package/docs/usage-guide.md +113 -0
- package/install.js +1044 -0
- package/package.json +35 -0
- package/pyproject.toml +182 -0
- package/rules/_bootstrap.md +138 -0
- package/rules/agents-ia.md +607 -0
- package/rules/api-design.md +337 -0
- package/rules/automations-n8n.md +646 -0
- package/rules/code-style.md +570 -0
- package/rules/documentation.md +98 -0
- package/rules/security.md +316 -0
- package/rules/stack.md +395 -0
- package/rules/testing.md +326 -0
- package/rules/workflow.md +353 -0
- package/scripts/create_skill.js +300 -0
- package/scripts/validate_skills.js +283 -0
- package/skills/ai-agent-engineer/SKILL.md +394 -0
- package/skills/ai-agent-engineer/references/agent-patterns.md +149 -0
- package/skills/api-designer/SKILL.md +429 -0
- package/skills/api-designer/references/api-standards.md +13 -0
- package/skills/architect/SKILL.md +285 -0
- package/skills/architect/references/c4-model.md +133 -0
- package/skills/automation-engineer/SKILL.md +352 -0
- package/skills/automation-engineer/references/n8n-patterns.md +127 -0
- package/skills/backend-engineer/SKILL.md +261 -0
- package/skills/backend-engineer/assets/fastapi-project-structure.yaml +74 -0
- package/skills/backend-engineer/references/debugging-guide.md +174 -0
- package/skills/backend-engineer/references/design-patterns.md +208 -0
- package/skills/backend-engineer/scripts/scaffold_backend.py +313 -0
- package/skills/bmad-methodology/SKILL.md +202 -0
- package/skills/bmad-methodology/references/scale-adaptive-levels.md +141 -0
- package/skills/browser-agent/SKILL.md +502 -0
- package/skills/browser-agent/scripts/playwright_setup.ts +16 -0
- package/skills/code-reviewer/SKILL.md +306 -0
- package/skills/code-reviewer/references/code-review-checklist.md +16 -0
- package/skills/data-engineer/SKILL.md +474 -0
- package/skills/data-engineer/assets/pg-monitoring-queries.sql +154 -0
- package/skills/data-engineer/references/index-strategy.md +128 -0
- package/skills/data-engineer/scripts/backup_postgres.py +221 -0
- package/skills/devops-engineer/SKILL.md +547 -0
- package/skills/devops-engineer/references/ci-cd-patterns.md +265 -0
- package/skills/devops-engineer/scripts/docker_healthcheck.py +125 -0
- package/skills/document-generator/SKILL.md +746 -0
- package/skills/document-generator/references/pdf-generation.md +22 -0
- package/skills/frontend-engineer/SKILL.md +532 -0
- package/skills/frontend-engineer/references/accessibility-guide.md +146 -0
- package/skills/frontend-engineer/scripts/audit_bundle.py +144 -0
- package/skills/git-workflow/SKILL.md +374 -0
- package/skills/git-workflow/references/git-flow.md +25 -0
- package/skills/mcp-builder/SKILL.md +471 -0
- package/skills/mcp-builder/references/mcp-server-guide.md +23 -0
- package/skills/mobile-engineer/SKILL.md +502 -0
- package/skills/mobile-engineer/references/platform-guidelines.md +160 -0
- package/skills/orchestrator/SKILL.md +246 -0
- package/skills/orchestrator/references/methodology-routing.md +117 -0
- package/skills/orchestrator/references/persona-mapping.md +85 -0
- package/skills/orchestrator/references/routing-logic.md +110 -0
- package/skills/performance-engineer/SKILL.md +549 -0
- package/skills/performance-engineer/references/caching-patterns.md +181 -0
- package/skills/performance-engineer/scripts/profile_endpoint.py +170 -0
- package/skills/product-manager/SKILL.md +488 -0
- package/skills/product-manager/references/prioritization-frameworks.md +126 -0
- package/skills/prompt-engineer/SKILL.md +433 -0
- package/skills/prompt-engineer/references/prompt-patterns.md +158 -0
- package/skills/qa-engineer/SKILL.md +441 -0
- package/skills/qa-engineer/references/testing-strategy.md +166 -0
- package/skills/qa-engineer/scripts/run_coverage.py +147 -0
- package/skills/scrum-master/SKILL.md +225 -0
- package/skills/scrum-master/references/sprint-ceremonies.md +159 -0
- package/skills/security-analyst/SKILL.md +390 -0
- package/skills/security-analyst/references/owasp-top10.md +188 -0
- package/skills/security-analyst/scripts/audit_security.py +242 -0
- package/skills/seo-auditor/SKILL.md +523 -0
- package/skills/seo-auditor/references/seo-checklist.md +17 -0
- package/skills/spec-driven-dev/SKILL.md +342 -0
- package/skills/spec-driven-dev/references/phase-gates.md +107 -0
- package/skills/supabase-expert/SKILL.md +602 -0
- package/skills/supabase-expert/references/supabase-patterns.md +19 -0
- package/skills/swe-agent/SKILL.md +311 -0
- package/skills/swe-agent/references/trajectory-format.md +134 -0
- package/skills/systematic-debugger/SKILL.md +512 -0
- package/skills/systematic-debugger/references/debugging-guide.md +12 -0
- package/skills/tech-lead/SKILL.md +409 -0
- package/skills/tech-lead/references/code-review-checklist.md +111 -0
- package/skills/technical-writer/SKILL.md +631 -0
- package/skills/technical-writer/references/doc-templates.md +218 -0
- package/skills/testing-strategist/SKILL.md +476 -0
- package/skills/testing-strategist/references/testing-pyramid.md +16 -0
- package/skills/ux-ui-designer/SKILL.md +419 -0
- package/skills/ux-ui-designer/references/design-system-foundation.md +168 -0
- package/skills_overview.txt +94 -0
- package/templates/PROJECT_KICKOFF.md +284 -0
- package/templates/SKILL_TEMPLATE.md +131 -0
- package/templates/USAGE.md +95 -0
- package/templates/agent-python/README.md +71 -0
- package/templates/agent-python/agent.py +272 -0
- package/templates/agent-python/config.yaml +76 -0
- package/templates/agent-python/prompts/system.md +109 -0
- package/templates/agent-python/requirements.txt +7 -0
- package/templates/automation-n8n/README.md +14 -0
- package/templates/automation-n8n/webhook-handler.json +57 -0
- package/templates/backend-node/Dockerfile +12 -0
- package/templates/backend-node/README.md +15 -0
- package/templates/backend-node/package.json +30 -0
- package/templates/backend-node/src/index.ts +19 -0
- package/templates/backend-node/src/routes.ts +7 -0
- package/templates/backend-node/tsconfig.json +22 -0
- package/templates/backend-python/Dockerfile +11 -0
- package/templates/backend-python/README.md +78 -0
- package/templates/backend-python/app/core/config.py +12 -0
- package/templates/backend-python/app/core/database.py +12 -0
- package/templates/backend-python/app/main.py +17 -0
- package/templates/backend-python/app/routers/__init__.py +1 -0
- package/templates/backend-python/app/routers/health.py +7 -0
- package/templates/backend-python/requirements-dev.txt +6 -0
- package/templates/backend-python/requirements.txt +4 -0
- package/templates/backend-python/tests/test_health.py +9 -0
- package/templates/checkpoint.yaml +117 -0
- package/templates/database/README.md +474 -0
- package/templates/frontend-react/README.md +446 -0
- package/templates/plan.yaml +320 -0
- package/templates/session.yaml +125 -0
- package/templates/spec.yaml +229 -0
- package/templates/tasks.yaml +330 -0
- package/workflows/bugfix-backend.md +380 -0
- package/workflows/documentation.md +232 -0
- package/workflows/generate-prd.md +320 -0
- package/workflows/ideation.md +396 -0
- package/workflows/new-agent-ia.md +497 -0
- package/workflows/new-automation.md +374 -0
- package/workflows/new-feature.md +290 -0
- package/workflows/optimize-performance.md +373 -0
- package/workflows/resolve-github-issue.md +524 -0
- package/workflows/security-review.md +291 -0
- package/workflows/spec-driven.md +476 -0
- package/workflows/testing-strategy.md +296 -0
- package/workflows/third-party-integration.md +277 -0
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
# Reglas para Agentes de IA - LMAgent
|
|
2
|
+
|
|
3
|
+
> **Tipo**: `rule` | **Versión**: 2.1 | **Actualización**: 2026-01
|
|
4
|
+
|
|
5
|
+
## 📌 Quick Reference
|
|
6
|
+
|
|
7
|
+
| Principio | Regla |
|
|
8
|
+
|-----------|-------|
|
|
9
|
+
| **Tool-first** | El LLM decide, las tools ejecutan. NUNCA ejecutar código directo del LLM. |
|
|
10
|
+
| **Stateless** | Agentes sin estado en memoria. Usar Redis para persistencia. |
|
|
11
|
+
| **Observable** | Logging de TODAS las interacciones LLM + Cost Tracking obligatorio. |
|
|
12
|
+
| **MCP Standard** | Usar Model Context Protocol (MCP) para definir herramientas. |
|
|
13
|
+
| **Guardrails** | Timeout, rate limit y validación de outputs OBLIGATORIOS. |
|
|
14
|
+
|
|
15
|
+
### 👥 Roles que usan esta regla
|
|
16
|
+
`ai-agent-engineer`, `prompt-engineer`, `backend-engineer`, `architect`
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
Este documento define las reglas y mejores prácticas para el desarrollo de agentes de IA.
|
|
21
|
+
|
|
22
|
+
## Principios Fundamentales
|
|
23
|
+
|
|
24
|
+
### 1. Separación de Responsabilidades
|
|
25
|
+
```
|
|
26
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
27
|
+
│ AGENTE = Orquestador de Herramientas │
|
|
28
|
+
│ │
|
|
29
|
+
│ ┌─────────────┐ │
|
|
30
|
+
│ │ LLM │ ← Razonamiento y decisiones │
|
|
31
|
+
│ └──────┬──────┘ │
|
|
32
|
+
│ │ │
|
|
33
|
+
│ ▼ │
|
|
34
|
+
│ ┌─────────────┐ │
|
|
35
|
+
│ │ Tools │ ← Acciones concretas │
|
|
36
|
+
│ └──────┬──────┘ │
|
|
37
|
+
│ │ │
|
|
38
|
+
│ ▼ │
|
|
39
|
+
│ ┌─────────────┐ │
|
|
40
|
+
│ │ State │ ← Memoria y contexto (Redis) │
|
|
41
|
+
│ └─────────────┘ │
|
|
42
|
+
└─────────────────────────────────────────────────────────────┘
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 2. Stateless Agents
|
|
46
|
+
- Los agentes NO deben almacenar estado en memoria
|
|
47
|
+
- Usar Redis para estado entre invocaciones
|
|
48
|
+
- Cada llamada debe ser autocontenida
|
|
49
|
+
|
|
50
|
+
### 3. Observable Everything
|
|
51
|
+
- Logging de todas las interacciones con LLM
|
|
52
|
+
- Tracking de costos por sesión
|
|
53
|
+
- Métricas de latencia y errores
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Estructura de Agentes
|
|
58
|
+
|
|
59
|
+
### Directorio
|
|
60
|
+
```
|
|
61
|
+
agents/
|
|
62
|
+
├── config/
|
|
63
|
+
│ ├── agents.yaml # Definición de agentes
|
|
64
|
+
│ ├── tools.yaml # Registry de herramientas
|
|
65
|
+
│ └── models.yaml # Configuración de LLMs
|
|
66
|
+
├── python/
|
|
67
|
+
│ ├── __init__.py
|
|
68
|
+
│ ├── base_agent.py # Clase base
|
|
69
|
+
│ ├── agent_runner.py # Ejecutor
|
|
70
|
+
│ ├── tools/ # Herramientas
|
|
71
|
+
│ │ ├── __init__.py
|
|
72
|
+
│ │ ├── registry.py # Registry de tools
|
|
73
|
+
│ │ ├── base_tool.py # Interface base
|
|
74
|
+
│ │ ├── http_tool.py
|
|
75
|
+
│ │ ├── database_tool.py
|
|
76
|
+
│ │ └── ...
|
|
77
|
+
│ ├── prompts/ # Templates de prompts
|
|
78
|
+
│ │ ├── system/
|
|
79
|
+
│ │ └── personas/
|
|
80
|
+
│ └── llm/ # Providers
|
|
81
|
+
│ ├── base_provider.py
|
|
82
|
+
│ ├── openai_provider.py
|
|
83
|
+
│ └── ...
|
|
84
|
+
└── README.md
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Diseño de Herramientas
|
|
90
|
+
|
|
91
|
+
### Interface Base
|
|
92
|
+
```python
|
|
93
|
+
from abc import ABC, abstractmethod
|
|
94
|
+
from pydantic import BaseModel, Field
|
|
95
|
+
from typing import Any, Dict
|
|
96
|
+
|
|
97
|
+
class ToolResult(BaseModel):
|
|
98
|
+
"""Resultado estándar de una herramienta."""
|
|
99
|
+
success: bool
|
|
100
|
+
data: Any = None
|
|
101
|
+
error: str | None = None
|
|
102
|
+
metadata: Dict[str, Any] = {}
|
|
103
|
+
|
|
104
|
+
class BaseTool(BaseModel, ABC):
|
|
105
|
+
"""Interface base para todas las herramientas."""
|
|
106
|
+
|
|
107
|
+
name: str
|
|
108
|
+
description: str
|
|
109
|
+
|
|
110
|
+
@abstractmethod
|
|
111
|
+
async def execute(self, **kwargs) -> ToolResult:
|
|
112
|
+
"""Ejecuta la herramienta."""
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
def to_function_schema(self) -> Dict:
|
|
116
|
+
"""Genera schema para function calling de OpenAI/Anthropic."""
|
|
117
|
+
pass
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Reglas de Diseño de Tools
|
|
121
|
+
|
|
122
|
+
✅ **Hacer:**
|
|
123
|
+
- Una herramienta = una responsabilidad
|
|
124
|
+
- Parámetros opcionales con defaults sensatos
|
|
125
|
+
- Retornar siempre `ToolResult` consistente
|
|
126
|
+
- Incluir descripción clara para el LLM
|
|
127
|
+
- Logging de todas las ejecuciones
|
|
128
|
+
- Timeouts explícitos
|
|
129
|
+
|
|
130
|
+
❌ **No hacer:**
|
|
131
|
+
- Tools que hacen múltiples cosas
|
|
132
|
+
- Parámetros sin tipos o descripciones
|
|
133
|
+
- Excepciones no manejadas
|
|
134
|
+
- Llamadas externas sin timeout
|
|
135
|
+
- Logs con datos sensibles
|
|
136
|
+
|
|
137
|
+
### Ejemplo de Tool Bien Diseñada
|
|
138
|
+
```python
|
|
139
|
+
from lmagent.tools.base import BaseTool, ToolResult
|
|
140
|
+
from pydantic import Field
|
|
141
|
+
from typing import Optional
|
|
142
|
+
import httpx
|
|
143
|
+
import structlog
|
|
144
|
+
|
|
145
|
+
logger = structlog.get_logger()
|
|
146
|
+
|
|
147
|
+
class HttpRequestTool(BaseTool):
|
|
148
|
+
"""
|
|
149
|
+
Realiza requests HTTP a APIs externas.
|
|
150
|
+
|
|
151
|
+
Usa esta herramienta cuando necesites:
|
|
152
|
+
- Obtener datos de una API externa
|
|
153
|
+
- Enviar datos a un webhook
|
|
154
|
+
- Verificar disponibilidad de un servicio
|
|
155
|
+
|
|
156
|
+
NO uses para:
|
|
157
|
+
- Acceso a base de datos (usa database_query)
|
|
158
|
+
- Operaciones en Redis (usa redis_cache)
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
name: str = "http_request"
|
|
162
|
+
description: str = "Make HTTP requests to external APIs"
|
|
163
|
+
|
|
164
|
+
async def execute(
|
|
165
|
+
self,
|
|
166
|
+
url: str = Field(..., description="Full URL to request"),
|
|
167
|
+
method: str = Field("GET", description="HTTP method"),
|
|
168
|
+
headers: Optional[dict] = Field(None, description="Request headers"),
|
|
169
|
+
body: Optional[dict] = Field(None, description="Request body for POST/PUT"),
|
|
170
|
+
timeout: int = Field(30, description="Timeout in seconds")
|
|
171
|
+
) -> ToolResult:
|
|
172
|
+
"""Ejecuta request HTTP."""
|
|
173
|
+
logger.info(
|
|
174
|
+
"http_request_start",
|
|
175
|
+
url=url,
|
|
176
|
+
method=method
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
181
|
+
response = await client.request(
|
|
182
|
+
method=method,
|
|
183
|
+
url=url,
|
|
184
|
+
headers=headers,
|
|
185
|
+
json=body
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
logger.info(
|
|
189
|
+
"http_request_complete",
|
|
190
|
+
url=url,
|
|
191
|
+
status_code=response.status_code
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
return ToolResult(
|
|
195
|
+
success=response.is_success,
|
|
196
|
+
data={
|
|
197
|
+
"status_code": response.status_code,
|
|
198
|
+
"body": response.json() if response.headers.get("content-type", "").startswith("application/json") else response.text,
|
|
199
|
+
"headers": dict(response.headers)
|
|
200
|
+
},
|
|
201
|
+
metadata={
|
|
202
|
+
"elapsed_ms": response.elapsed.total_seconds() * 1000
|
|
203
|
+
}
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
except httpx.TimeoutException:
|
|
207
|
+
logger.warning("http_request_timeout", url=url, timeout=timeout)
|
|
208
|
+
return ToolResult(
|
|
209
|
+
success=False,
|
|
210
|
+
error=f"Request timed out after {timeout}s",
|
|
211
|
+
metadata={"suggestion": "Try increasing timeout or check if service is available"}
|
|
212
|
+
)
|
|
213
|
+
except Exception as e:
|
|
214
|
+
logger.error("http_request_error", url=url, error=str(e))
|
|
215
|
+
return ToolResult(
|
|
216
|
+
success=False,
|
|
217
|
+
error=str(e)
|
|
218
|
+
)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Diseño de Prompts
|
|
224
|
+
|
|
225
|
+
### System Prompt Template
|
|
226
|
+
```markdown
|
|
227
|
+
You are {agent_name}, an AI assistant specialized in {domain}.
|
|
228
|
+
|
|
229
|
+
## Your Role
|
|
230
|
+
{role_description}
|
|
231
|
+
|
|
232
|
+
## Available Tools
|
|
233
|
+
You have access to the following tools:
|
|
234
|
+
{tools_list}
|
|
235
|
+
|
|
236
|
+
When using tools, always:
|
|
237
|
+
1. Think about which tool is most appropriate
|
|
238
|
+
2. Provide all required parameters
|
|
239
|
+
3. Handle errors gracefully
|
|
240
|
+
|
|
241
|
+
## Response Guidelines
|
|
242
|
+
- Be concise and direct
|
|
243
|
+
- Use structured formats when appropriate
|
|
244
|
+
- Cite sources when using external data
|
|
245
|
+
- Admit when you don't know something
|
|
246
|
+
|
|
247
|
+
## Constraints
|
|
248
|
+
- Maximum {max_tokens} tokens per response
|
|
249
|
+
- Do not make up information
|
|
250
|
+
- Do not access tools outside your allowed set
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Mejores Prácticas de Prompts
|
|
254
|
+
|
|
255
|
+
1. **Sea específico**: Evitar instrucciones vagas
|
|
256
|
+
```
|
|
257
|
+
❌ "Ayuda al usuario"
|
|
258
|
+
✅ "Ayuda al usuario a encontrar información sobre productos.
|
|
259
|
+
Si no encuentras el producto, sugiere alternativas similares."
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
2. **Dé ejemplos (Few-shot)**:
|
|
263
|
+
```
|
|
264
|
+
## Examples
|
|
265
|
+
|
|
266
|
+
User: "Find product with SKU ABC123"
|
|
267
|
+
Assistant: I'll search for that product.
|
|
268
|
+
[Uses: search_product(sku="ABC123")]
|
|
269
|
+
Result: Found "Widget Pro" priced at $99.99
|
|
270
|
+
|
|
271
|
+
User: "What's the status of order 789?"
|
|
272
|
+
Assistant: Let me check that order.
|
|
273
|
+
[Uses: get_order(order_id="789")]
|
|
274
|
+
Result: Order is "shipped", tracking: XYZ...
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
3. **Estructure el output**:
|
|
278
|
+
```
|
|
279
|
+
## Output Format
|
|
280
|
+
|
|
281
|
+
Always respond in this JSON format:
|
|
282
|
+
{
|
|
283
|
+
"thinking": "Your reasoning process",
|
|
284
|
+
"action": "Tool name or 'respond'",
|
|
285
|
+
"action_input": {...} or null,
|
|
286
|
+
"response": "Final response to user" or null
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
4. **Defina límites claros**:
|
|
291
|
+
```
|
|
292
|
+
## You CANNOT:
|
|
293
|
+
- Access production databases directly
|
|
294
|
+
- Send emails without user confirmation
|
|
295
|
+
- Make purchases or financial transactions
|
|
296
|
+
- Access files outside the project directory
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## Manejo de Errores
|
|
302
|
+
|
|
303
|
+
### Estrategia de Reintentos
|
|
304
|
+
```python
|
|
305
|
+
from tenacity import retry, stop_after_attempt, wait_exponential
|
|
306
|
+
|
|
307
|
+
class AgentRunner:
|
|
308
|
+
@retry(
|
|
309
|
+
stop=stop_after_attempt(3),
|
|
310
|
+
wait=wait_exponential(multiplier=1, min=1, max=10)
|
|
311
|
+
)
|
|
312
|
+
async def call_llm(self, messages: list) -> LLMResponse:
|
|
313
|
+
"""Llama al LLM con reintentos automáticos."""
|
|
314
|
+
return await self.llm_provider.complete(messages)
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### Fallback de Modelos
|
|
318
|
+
```python
|
|
319
|
+
async def get_completion(self, messages: list) -> str:
|
|
320
|
+
"""Intenta con modelo principal, fallback a secundario."""
|
|
321
|
+
try:
|
|
322
|
+
return await self.primary_model.complete(messages)
|
|
323
|
+
except RateLimitError:
|
|
324
|
+
logger.warning("primary_model_rate_limited, using fallback")
|
|
325
|
+
return await self.fallback_model.complete(messages)
|
|
326
|
+
except Exception as e:
|
|
327
|
+
logger.error("llm_error", error=str(e))
|
|
328
|
+
raise
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Errores del Agente
|
|
332
|
+
```python
|
|
333
|
+
class AgentError(Exception):
|
|
334
|
+
"""Error base de agentes."""
|
|
335
|
+
pass
|
|
336
|
+
|
|
337
|
+
class ToolExecutionError(AgentError):
|
|
338
|
+
"""Error ejecutando herramienta."""
|
|
339
|
+
def __init__(self, tool_name: str, error: str):
|
|
340
|
+
self.tool_name = tool_name
|
|
341
|
+
super().__init__(f"Tool '{tool_name}' failed: {error}")
|
|
342
|
+
|
|
343
|
+
class CostLimitExceeded(AgentError):
|
|
344
|
+
"""Se excedió el límite de costo."""
|
|
345
|
+
def __init__(self, current_cost: float, limit: float):
|
|
346
|
+
self.current_cost = current_cost
|
|
347
|
+
self.limit = limit
|
|
348
|
+
super().__init__(f"Cost ${current_cost:.2f} exceeded limit ${limit:.2f}")
|
|
349
|
+
|
|
350
|
+
class MaxIterationsExceeded(AgentError):
|
|
351
|
+
"""Se excedió el número máximo de iteraciones."""
|
|
352
|
+
pass
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
## Observabilidad
|
|
358
|
+
|
|
359
|
+
### Logging Estructurado
|
|
360
|
+
```python
|
|
361
|
+
import structlog
|
|
362
|
+
|
|
363
|
+
logger = structlog.get_logger()
|
|
364
|
+
|
|
365
|
+
class Agent:
|
|
366
|
+
async def run(self, user_input: str) -> str:
|
|
367
|
+
run_id = generate_run_id()
|
|
368
|
+
|
|
369
|
+
logger.info(
|
|
370
|
+
"agent_run_start",
|
|
371
|
+
run_id=run_id,
|
|
372
|
+
agent_name=self.name,
|
|
373
|
+
input_length=len(user_input)
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
try:
|
|
377
|
+
result = await self._execute(user_input)
|
|
378
|
+
|
|
379
|
+
logger.info(
|
|
380
|
+
"agent_run_complete",
|
|
381
|
+
run_id=run_id,
|
|
382
|
+
iterations=self.iteration_count,
|
|
383
|
+
total_cost=self.cost_tracker.total,
|
|
384
|
+
output_length=len(result)
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
return result
|
|
388
|
+
|
|
389
|
+
except Exception as e:
|
|
390
|
+
logger.error(
|
|
391
|
+
"agent_run_error",
|
|
392
|
+
run_id=run_id,
|
|
393
|
+
error=str(e),
|
|
394
|
+
error_type=type(e).__name__
|
|
395
|
+
)
|
|
396
|
+
raise
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### Trajectory Logging
|
|
400
|
+
```python
|
|
401
|
+
class TrajectoryLogger:
|
|
402
|
+
"""Registra el historial completo de una ejecución."""
|
|
403
|
+
|
|
404
|
+
def __init__(self, run_id: str):
|
|
405
|
+
self.run_id = run_id
|
|
406
|
+
self.steps: list[TrajectoryStep] = []
|
|
407
|
+
|
|
408
|
+
def log_step(
|
|
409
|
+
self,
|
|
410
|
+
step_num: int,
|
|
411
|
+
thought: str,
|
|
412
|
+
action: str,
|
|
413
|
+
action_input: dict,
|
|
414
|
+
observation: str
|
|
415
|
+
):
|
|
416
|
+
step = TrajectoryStep(
|
|
417
|
+
step=step_num,
|
|
418
|
+
timestamp=datetime.utcnow(),
|
|
419
|
+
thought=thought,
|
|
420
|
+
action=action,
|
|
421
|
+
action_input=action_input,
|
|
422
|
+
observation=observation
|
|
423
|
+
)
|
|
424
|
+
self.steps.append(step)
|
|
425
|
+
|
|
426
|
+
# Log visual para debugging
|
|
427
|
+
print(f"🤠 INFO STEP {step_num}")
|
|
428
|
+
print(f"💭 THOUGHT: {thought}")
|
|
429
|
+
print(f"🎬 ACTION: {action}")
|
|
430
|
+
print(f"📤 OBSERVATION: {observation[:200]}...")
|
|
431
|
+
|
|
432
|
+
def save(self, path: str):
|
|
433
|
+
"""Guarda trajectory para análisis posterior."""
|
|
434
|
+
with open(path, 'w') as f:
|
|
435
|
+
json.dump([s.model_dump() for s in self.steps], f, indent=2)
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### Cost Tracking
|
|
439
|
+
```python
|
|
440
|
+
class CostTracker:
|
|
441
|
+
"""Monitorea costos de API por sesión."""
|
|
442
|
+
|
|
443
|
+
def __init__(self, max_cost: float = 2.00):
|
|
444
|
+
self.max_cost = max_cost
|
|
445
|
+
self.total = 0.0
|
|
446
|
+
self.calls: list[CostEntry] = []
|
|
447
|
+
|
|
448
|
+
def track(
|
|
449
|
+
self,
|
|
450
|
+
model: str,
|
|
451
|
+
input_tokens: int,
|
|
452
|
+
output_tokens: int
|
|
453
|
+
) -> float:
|
|
454
|
+
"""Registra y valida costo de llamada."""
|
|
455
|
+
cost = self._calculate_cost(model, input_tokens, output_tokens)
|
|
456
|
+
|
|
457
|
+
self.calls.append(CostEntry(
|
|
458
|
+
model=model,
|
|
459
|
+
input_tokens=input_tokens,
|
|
460
|
+
output_tokens=output_tokens,
|
|
461
|
+
cost=cost,
|
|
462
|
+
timestamp=datetime.utcnow()
|
|
463
|
+
))
|
|
464
|
+
|
|
465
|
+
self.total += cost
|
|
466
|
+
|
|
467
|
+
if self.total >= self.max_cost:
|
|
468
|
+
raise CostLimitExceeded(self.total, self.max_cost)
|
|
469
|
+
|
|
470
|
+
return cost
|
|
471
|
+
|
|
472
|
+
def _calculate_cost(
|
|
473
|
+
self,
|
|
474
|
+
model: str,
|
|
475
|
+
input_tokens: int,
|
|
476
|
+
output_tokens: int
|
|
477
|
+
) -> float:
|
|
478
|
+
"""Calcula costo según modelo."""
|
|
479
|
+
# Ver config/models.yaml para precios
|
|
480
|
+
prices = PRICING.get(model, {"input": 0.01, "output": 0.03})
|
|
481
|
+
return (
|
|
482
|
+
(input_tokens / 1000) * prices["input"] +
|
|
483
|
+
(output_tokens / 1000) * prices["output"]
|
|
484
|
+
)
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
---
|
|
488
|
+
|
|
489
|
+
## Seguridad
|
|
490
|
+
|
|
491
|
+
### Guardrails Obligatorios
|
|
492
|
+
|
|
493
|
+
1. **Validar outputs de LLM** antes de ejecutar
|
|
494
|
+
2. **Limitar herramientas** a las necesarias
|
|
495
|
+
3. **Rate limiting** por usuario/sesión
|
|
496
|
+
4. **Sanitizar inputs** del usuario
|
|
497
|
+
5. **No exponer** API keys en logs
|
|
498
|
+
|
|
499
|
+
### Sandbox para Ejecución de Código
|
|
500
|
+
```python
|
|
501
|
+
class SandboxTool(BaseTool):
|
|
502
|
+
"""Ejecuta código en ambiente aislado."""
|
|
503
|
+
|
|
504
|
+
name: str = "sandbox_execute"
|
|
505
|
+
description: str = "Execute code in isolated Docker container"
|
|
506
|
+
|
|
507
|
+
# Límites de seguridad
|
|
508
|
+
MAX_MEMORY = "256m"
|
|
509
|
+
MAX_CPU = "0.5"
|
|
510
|
+
MAX_TIMEOUT = 300
|
|
511
|
+
NETWORK = "none" # Sin acceso a red
|
|
512
|
+
|
|
513
|
+
async def execute(
|
|
514
|
+
self,
|
|
515
|
+
code: str,
|
|
516
|
+
language: str = "python",
|
|
517
|
+
timeout: int = 30
|
|
518
|
+
) -> ToolResult:
|
|
519
|
+
# Sanitizar código
|
|
520
|
+
if self._is_dangerous(code):
|
|
521
|
+
return ToolResult(
|
|
522
|
+
success=False,
|
|
523
|
+
error="Code contains potentially dangerous operations"
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
# Ejecutar en container
|
|
527
|
+
result = await self._run_in_docker(
|
|
528
|
+
code=code,
|
|
529
|
+
language=language,
|
|
530
|
+
timeout=min(timeout, self.MAX_TIMEOUT)
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
return result
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
---
|
|
537
|
+
|
|
538
|
+
## Integración con n8n
|
|
539
|
+
|
|
540
|
+
Los agentes deben exponerse como endpoints HTTP para que n8n pueda invocarlos:
|
|
541
|
+
|
|
542
|
+
```python
|
|
543
|
+
from fastapi import APIRouter
|
|
544
|
+
from pydantic import BaseModel
|
|
545
|
+
|
|
546
|
+
router = APIRouter(prefix="/agents", tags=["agents"])
|
|
547
|
+
|
|
548
|
+
class AgentRequest(BaseModel):
|
|
549
|
+
agent_name: str
|
|
550
|
+
input: str
|
|
551
|
+
context: dict = {}
|
|
552
|
+
max_cost: float = 2.00
|
|
553
|
+
|
|
554
|
+
class AgentResponse(BaseModel):
|
|
555
|
+
success: bool
|
|
556
|
+
output: str
|
|
557
|
+
cost: float
|
|
558
|
+
iterations: int
|
|
559
|
+
trajectory_id: str | None = None
|
|
560
|
+
|
|
561
|
+
@router.post("/run", response_model=AgentResponse)
|
|
562
|
+
async def run_agent(request: AgentRequest) -> AgentResponse:
|
|
563
|
+
"""
|
|
564
|
+
Ejecuta un agente con el input dado.
|
|
565
|
+
|
|
566
|
+
Diseñado para ser llamado desde n8n via HTTP Request node.
|
|
567
|
+
"""
|
|
568
|
+
agent = await load_agent(request.agent_name)
|
|
569
|
+
agent.cost_tracker.max_cost = request.max_cost
|
|
570
|
+
|
|
571
|
+
result = await agent.run(request.input, context=request.context)
|
|
572
|
+
|
|
573
|
+
return AgentResponse(
|
|
574
|
+
success=True,
|
|
575
|
+
output=result,
|
|
576
|
+
cost=agent.cost_tracker.total,
|
|
577
|
+
iterations=agent.iteration_count,
|
|
578
|
+
trajectory_id=agent.trajectory_logger.run_id
|
|
579
|
+
)
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
---
|
|
583
|
+
|
|
584
|
+
## ✅ Checklist de Validación (Antes de Deploy)
|
|
585
|
+
|
|
586
|
+
### Diseño del Agente
|
|
587
|
+
- [ ] Arquitectura elegida y documentada (ReAct, Tool-only, etc.)
|
|
588
|
+
- [ ] Tools definidas con schemas MCP/Pydantic estrictos
|
|
589
|
+
- [ ] System Prompt aprobado por /prompt engineer
|
|
590
|
+
|
|
591
|
+
### Seguridad (Guardrails)
|
|
592
|
+
- [ ] Timeout configurado en TODAS las tools
|
|
593
|
+
- [ ] Rate limit por usuario/sesión
|
|
594
|
+
- [ ] Cost limit configurado (default: $2.00)
|
|
595
|
+
- [ ] Outputs del LLM validados antes de ejecutar
|
|
596
|
+
- [ ] Sin API keys en logs
|
|
597
|
+
|
|
598
|
+
### Observabilidad
|
|
599
|
+
- [ ] Logging estructurado con structlog
|
|
600
|
+
- [ ] Trajectory logging habilitado
|
|
601
|
+
- [ ] Cost tracking implementado
|
|
602
|
+
- [ ] Métricas de latencia expuestas
|
|
603
|
+
|
|
604
|
+
### Integración
|
|
605
|
+
- [ ] Endpoint HTTP para n8n creado
|
|
606
|
+
- [ ] Documentación de API generada
|
|
607
|
+
- [ ] Tests de evals pasando (Faithfulness > 0.7)
|