feat(F): F-CTX context_builder, F-STR agent_stream, F-DEF agent definition fields, F-SKILL skill_registry, F-TOOL agent_tools
Check Cross-Plugin Imports / check (push) Has been cancelled

- F-CTX: app/ai/context_builder.py (282 lines) — build_agent_context() + ReActSystemPromptBuilder
- F-STR: app/ai/agent_stream.py (155 lines) — stream_react_loop() with SSE events (step, status, done, error)
- F-DEF: AgentDefinition fields added (temperature, max_tokens, max_steps, trace_mode, skill_ids, trigger_config, ai_use_case_metadata) + migration 0122
- F-SKILL: app/ai/skill_registry.py (82 lines) — SkillDefinition + SkillRegistry singleton
- F-TOOL: app/ai/agent_tools.py (117 lines) — get_agent_tools() with permission intersection
- Skill CRUD routes: app/plugins/builtins/automation/skill_routes.py
- Tests: test_skill_registry.py (97 lines), test_agent_tools.py (219 lines)
- All Python compile checks pass, tests require PostgreSQL (infra issue, not code bug)
This commit is contained in:
Agent Zero
2026-08-17 16:40:55 +02:00
parent c760b5961c
commit dbeadd8ab1
14 changed files with 1457 additions and 1 deletions
+144
View File
@@ -22,6 +22,7 @@ from app.plugins.builtins.automation.models import (
AutomationDefinition,
AutomationRun,
AutomationVersion,
SkillDefinitionDB,
)
logger = logging.getLogger(__name__)
@@ -114,6 +115,13 @@ class AgentService:
max_executions_per_hour=data.get("max_executions_per_hour", 10),
max_duration_seconds=data.get("max_duration_seconds", 300),
budget_limit_usd=data.get("budget_limit_usd", 1.0),
temperature=data.get("temperature", 0.3),
max_tokens=data.get("max_tokens", 1000),
max_steps=data.get("max_steps", 20),
trace_mode=data.get("trace_mode", "standard"),
skill_ids=data.get("skill_ids", []),
trigger_config=data.get("trigger_config", {}),
ai_use_case_metadata=data.get("ai_use_case_metadata", {}),
created_by=user_id,
owner_id=user_id,
)
@@ -137,6 +145,13 @@ class AgentService:
"max_executions_per_hour": agent.max_executions_per_hour,
"max_duration_seconds": agent.max_duration_seconds,
"budget_limit_usd": agent.budget_limit_usd,
"temperature": agent.temperature,
"max_tokens": agent.max_tokens,
"max_steps": agent.max_steps,
"trace_mode": agent.trace_mode,
"skill_ids": agent.skill_ids,
"trigger_config": agent.trigger_config,
"ai_use_case_metadata": agent.ai_use_case_metadata,
},
changed_by=user_id,
)
@@ -189,6 +204,13 @@ class AgentService:
"max_executions_per_hour": agent.max_executions_per_hour,
"max_duration_seconds": agent.max_duration_seconds,
"budget_limit_usd": agent.budget_limit_usd,
"temperature": agent.temperature,
"max_tokens": agent.max_tokens,
"max_steps": agent.max_steps,
"trace_mode": agent.trace_mode,
"skill_ids": agent.skill_ids,
"trigger_config": agent.trigger_config,
"ai_use_case_metadata": agent.ai_use_case_metadata,
},
changed_by=user_id,
)
@@ -750,3 +772,125 @@ class RunLogService:
.offset(offset)
)
return list(result.scalars().all()), total
# ─── Skill Service (Phase F-SKILL) ───
class SkillService:
"""CRUD for skill definitions.
Skills are orchestration metadata, NOT a permission source. They reference
tool IDs that the agent and the user must already be permitted to use.
"""
@staticmethod
async def list(
db: AsyncSession,
tenant_id: uuid.UUID,
is_active: bool | None = None,
category: str | None = None,
limit: int = 50,
offset: int = 0,
) -> tuple[list[SkillDefinitionDB], int]:
"""List skill definitions with optional filters."""
query = select(SkillDefinitionDB).where(
SkillDefinitionDB.tenant_id == tenant_id
)
count_query = (
select(func.count())
.select_from(SkillDefinitionDB)
.where(SkillDefinitionDB.tenant_id == tenant_id)
)
if is_active is not None:
query = query.where(SkillDefinitionDB.is_active == is_active)
count_query = count_query.where(SkillDefinitionDB.is_active == is_active)
if category is not None:
query = query.where(SkillDefinitionDB.category == category)
count_query = count_query.where(SkillDefinitionDB.category == category)
count_result = await db.execute(count_query)
total = count_result.scalar() or 0
result = await db.execute(
query.order_by(SkillDefinitionDB.created_at.desc())
.limit(limit)
.offset(offset)
)
return list(result.scalars().all()), total
@staticmethod
async def get_by_id(
db: AsyncSession, tenant_id: uuid.UUID, skill_id: uuid.UUID
) -> SkillDefinitionDB | None:
"""Get a single skill definition by ID."""
result = await db.execute(
select(SkillDefinitionDB)
.where(SkillDefinitionDB.id == skill_id)
.where(SkillDefinitionDB.tenant_id == tenant_id)
.limit(1)
)
return result.scalar_one_or_none()
@staticmethod
async def get_by_name(
db: AsyncSession, name: str, tenant_id: uuid.UUID | None = None
) -> SkillDefinitionDB | None:
"""Get a single skill definition by name (optionally scoped to tenant)."""
query = select(SkillDefinitionDB).where(SkillDefinitionDB.name == name)
if tenant_id is not None:
query = query.where(SkillDefinitionDB.tenant_id == tenant_id)
result = await db.execute(query.limit(1))
return result.scalar_one_or_none()
@staticmethod
async def create(
db: AsyncSession,
tenant_id: uuid.UUID,
data: dict[str, Any],
) -> SkillDefinitionDB:
"""Create a new skill definition."""
skill = SkillDefinitionDB(
tenant_id=tenant_id,
name=data["name"],
description=data.get("description", ""),
instructions=data.get("instructions", ""),
allowed_tool_ids=data.get("allowed_tool_ids", []),
context_policy=data.get("context_policy"),
category=data.get("category", "general"),
is_active=data.get("is_active", True),
)
db.add(skill)
await db.flush()
return skill
@staticmethod
async def update(
db: AsyncSession,
tenant_id: uuid.UUID,
skill_id: uuid.UUID,
data: dict[str, Any],
) -> SkillDefinitionDB | None:
"""Update an existing skill definition (partial)."""
skill = await SkillService.get_by_id(db, tenant_id, skill_id)
if skill is None:
return None
for key, value in data.items():
if hasattr(skill, key) and value is not None:
setattr(skill, key, value)
return skill
@staticmethod
async def delete(
db: AsyncSession, tenant_id: uuid.UUID, skill_id: uuid.UUID
) -> bool:
"""Delete a skill definition."""
skill = await SkillService.get_by_id(db, tenant_id, skill_id)
if skill is None:
return False
await db.delete(skill)
await db.flush()
return True