Enhance MCP development features and introduce skills management

- Added configuration options for requiring human approval before applying LLM-generated MCP patches.
- Updated Docker setup to include skills directory.
- Integrated skills management into the backend, allowing for procedural guides and skill matching.
- Refactored database initialization to apply Alembic migrations.
- Enhanced task approval process to handle MCP patch applications with optional approval.
- Introduced new schemas for skills and updated existing APIs to support skills functionality.

This commit lays the groundwork for improved agent capabilities and better management of MCP development processes.
This commit is contained in:
2026-06-14 22:27:24 +03:00
parent 6185b9b85a
commit 0375b20bb4
30 changed files with 1733 additions and 151 deletions

View File

@@ -37,6 +37,7 @@ from app.services.device_defaults import resolve_slot_defaults
from app.services.investigation_context import gather_investigation_context
from app.services.approval_filter import split_proposed_fixes
from app.services.troubleshooting_rules import rule_guidance_block
from app.services.skill_registry import match_skills, skill_guidance_block
from app.services.llm import triage_model, triage_model_info
from app.services.llm_usage import LlmUsageTracker, merge_run_history
from app.services.model_config import get_routing_config
@@ -75,6 +76,7 @@ class AgentState(TypedDict, total=False):
prior_context: dict | None
investigation_context: dict | None
matched_rule: dict | None
matched_skills: list[dict] | None
follow_up: str | None
run_number: int
prior_runs: list[dict]
@@ -348,6 +350,7 @@ async def triage_node(state: AgentState) -> AgentState:
f"Onboard devices to check: {json.dumps(device_summary)}"
f"{_investigation_context_block(state.get('investigation_context'))}"
f"{rule_guidance_block(state.get('matched_rule'))}"
f"{skill_guidance_block(state.get('matched_skills'))}"
f"{_prior_context_block(state.get('prior_context'), state.get('follow_up'))}"
)
triage_info = await triage_model_info()
@@ -449,6 +452,7 @@ async def reason_node(state: AgentState) -> AgentState:
f"Title: {state['title']}\nIssue: {state['issue']}\nTriage: {json.dumps(triage)}\n\n{diag_text}"
f"{_investigation_context_block(state.get('investigation_context'))}"
f"{rule_guidance_block(state.get('matched_rule'))}"
f"{skill_guidance_block(state.get('matched_skills'))}"
f"{_prior_context_block(state.get('prior_context'), state.get('follow_up'))}"
)
messages = [SystemMessage(content=prompt), HumanMessage(content=ctx)]
@@ -604,6 +608,10 @@ async def report_node(state: AgentState) -> AgentState:
}
if state.get("matched_rule"):
report["matched_rule"] = state["matched_rule"]
if state.get("matched_skills"):
report["matched_skills"] = [
{k: v for k, v in s.items() if k != "body"} for s in state["matched_skills"]
]
report = merge_run_history(state.get("prior_runs") or [], run_number, report, llm_summary)
@@ -786,6 +794,7 @@ async def run_task_agent(task_id: int) -> None:
devices: list[dict] = []
matched_rule: dict | None = None
matched_skills: list[dict] = []
vessel_name: str | None = None
vessel_site: str | None = None
vessel_public_ip: str | None = None
@@ -795,6 +804,18 @@ async def run_task_agent(task_id: int) -> None:
all_devices, routing_text, title=task.title
)
matched_rule = rule.as_dict() if rule else None
rule_id = matched_rule.get("id") if matched_rule else None
matched_skills = [
{
"id": s.id,
"name": s.name,
"description": s.description,
"body": s.body,
"score": s.score,
"priority": s.priority,
}
for s in match_skills(task.title, routing_text, matched_rule_id=rule_id)
]
vres = await db.execute(select(Vessel).where(Vessel.id == task.vessel_id))
vessel = vres.scalar_one_or_none()
if vessel:
@@ -807,6 +828,20 @@ async def run_task_agent(task_id: int) -> None:
if d:
devices = [_device_dict(d)]
if not matched_skills:
rule_id = matched_rule.get("id") if matched_rule else None
matched_skills = [
{
"id": s.id,
"name": s.name,
"description": s.description,
"body": s.body,
"score": s.score,
"priority": s.priority,
}
for s in match_skills(task.title, routing_text, matched_rule_id=rule_id)
]
task.status = TaskStatus.running
task.details = details
await db.commit()
@@ -842,6 +877,14 @@ async def run_task_agent(task_id: int) -> None:
f"Matched rule: {matched_rule.get('name')}{', '.join(matched_rule.get('devices') or [])}",
matched_rule,
)
if matched_skills:
names = ", ".join(s["name"] for s in matched_skills)
await _emit(
task_id,
"skill",
f"Matched skills: {names}",
{"skills": [{k: v for k, v in s.items() if k != "body"} for s in matched_skills]},
)
remaining = await balance.latest_remaining_usd()
from app.config import settings
@@ -873,6 +916,7 @@ async def run_task_agent(task_id: int) -> None:
"run_number": run_number,
"prior_runs": prior_runs if continue_mode else [],
"matched_rule": matched_rule,
"matched_skills": matched_skills or None,
}
try: