feat(knowledge-flywheel): add Phase 3 Knowledge Flywheel — AI analysis, review queue, analytics
Phase 3 implementation: - AI session analysis service that generates flow proposals from resolved sessions - APScheduler job for batch processing pending analyses (max_instances=1) - Knowledge gap detection (weak options, high escalation signals) - Flow proposals CRUD with team admin review workflow (approve/edit/dismiss/reject) - FlowPilot analytics dashboard with confidence tiers, PSA metrics, knowledge gaps - In-session script generator component - Review queue page with filtering and proposal detail panel Bug fixes from review (12 total): - Fix "Edit & Publish" navigating to non-existent /editor/new route - Hide Approve button for enhancement proposals (require Edit & Publish) - Add max_instances=1 to scheduler to prevent TOCTOU race - Fix eventual_success case() double-counting failed retries - Add tree_structure validation before creating tree from proposal - Simplify script generator rendering condition - Add severity style fallback, toFixed on rates, Link instead of <a href> - Add toast.warning on dismiss failure, fix dedup for domain-less sessions - Cast Decimal to int in knowledge gap evidence dicts Also updates CLAUDE.md with lessons 67-71 and Phase 3 project structure. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,7 @@ from datetime import datetime, timezone
|
||||
from typing import Any, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import select, or_
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
@@ -228,6 +228,11 @@ async def start_session(
|
||||
if ticket_context_block:
|
||||
ticket_prompt_section = f"\n## PSA TICKET CONTEXT\n{ticket_context_block}\n"
|
||||
|
||||
# Include available script templates for in-session script generation
|
||||
script_context = await _build_script_context(team_id, db)
|
||||
if script_context:
|
||||
ticket_prompt_section += f"\n{script_context}\n"
|
||||
|
||||
system_prompt = FLOWPILOT_SYSTEM_PROMPT.format(
|
||||
structured_output_schema=STRUCTURED_OUTPUT_SCHEMA,
|
||||
team_context=ticket_prompt_section,
|
||||
@@ -448,6 +453,9 @@ async def resolve_session(
|
||||
|
||||
documentation = _generate_documentation(session)
|
||||
|
||||
# Queue for Knowledge Flywheel analysis
|
||||
session.analysis_status = "pending"
|
||||
|
||||
await db.flush()
|
||||
|
||||
# Push documentation to PSA if ticket is linked
|
||||
@@ -909,6 +917,13 @@ def _create_step_from_parsed(
|
||||
if parsed["type"] == "action":
|
||||
content["action_type"] = parsed.get("action_type", "instruction")
|
||||
content["expected_outcome"] = parsed.get("expected_outcome")
|
||||
# Script generation fields (populated when FlowPilot suggests a script)
|
||||
if parsed.get("template_id"):
|
||||
content["template_id"] = parsed["template_id"]
|
||||
if parsed.get("pre_filled_params"):
|
||||
content["pre_filled_params"] = parsed["pre_filled_params"]
|
||||
if parsed.get("instructions"):
|
||||
content["instructions"] = parsed["instructions"]
|
||||
elif parsed["type"] == "resolution_suggestion":
|
||||
content["resolution_summary"] = parsed.get("resolution_summary")
|
||||
content["follow_up_recommendations"] = parsed.get("follow_up_recommendations", [])
|
||||
@@ -1066,6 +1081,51 @@ async def _process_ticket_intake(
|
||||
return None, None, "unavailable"
|
||||
|
||||
|
||||
async def _build_script_context(
|
||||
team_id: Optional[UUID],
|
||||
db: AsyncSession,
|
||||
) -> Optional[str]:
|
||||
"""Build script template context for the system prompt.
|
||||
|
||||
Includes available script templates so FlowPilot can suggest
|
||||
script_generation actions with pre-filled parameters.
|
||||
"""
|
||||
try:
|
||||
from app.models.script_template import ScriptTemplate
|
||||
|
||||
result = await db.execute(
|
||||
select(ScriptTemplate)
|
||||
.where(
|
||||
ScriptTemplate.is_active.is_(True),
|
||||
or_(
|
||||
ScriptTemplate.team_id.is_(None),
|
||||
ScriptTemplate.team_id == team_id,
|
||||
),
|
||||
)
|
||||
.order_by(ScriptTemplate.usage_count.desc())
|
||||
.limit(20)
|
||||
)
|
||||
templates = result.scalars().all()
|
||||
|
||||
if not templates:
|
||||
return None
|
||||
|
||||
lines = ["## AVAILABLE SCRIPTS"]
|
||||
lines.append("When the engineer needs to run a script, suggest an action with action_type='script_generation'.")
|
||||
lines.append("Include template_id and pre_filled_params based on the diagnostic context.\n")
|
||||
for t in templates:
|
||||
params = t.parameters_schema.get("parameters", [])
|
||||
param_keys = ", ".join(p.get("key", "") for p in params if p.get("key"))
|
||||
lines.append(f"- {t.name} (ID: {t.id}): {t.description or 'No description'}")
|
||||
if param_keys:
|
||||
lines.append(f" Parameters: {param_keys}")
|
||||
|
||||
return "\n".join(lines)
|
||||
except Exception as e:
|
||||
logger.warning("Failed to build script context: %s", e)
|
||||
return None
|
||||
|
||||
|
||||
async def _build_escalation_package_enhanced(
|
||||
session: AISession,
|
||||
user_id: UUID,
|
||||
|
||||
Reference in New Issue
Block a user