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