|
1
|
"""Planning agent loop for synthesizing knowledge into artifacts.""" |
|
2
|
|
|
3
|
import logging |
|
4
|
from pathlib import Path |
|
5
|
from typing import List |
|
6
|
|
|
7
|
from video_processor.agent.kb_context import KBContext |
|
8
|
from video_processor.agent.skills.base import ( |
|
9
|
AgentContext, |
|
10
|
Artifact, |
|
11
|
get_skill, |
|
12
|
list_skills, |
|
13
|
) |
|
14
|
|
|
15
|
logger = logging.getLogger(__name__) |
|
16
|
|
|
17
|
|
|
18
|
class PlanningAgent: |
|
19
|
"""AI agent that synthesizes knowledge into planning artifacts.""" |
|
20
|
|
|
21
|
def __init__(self, context: AgentContext): |
|
22
|
self.context = context |
|
23
|
|
|
24
|
@classmethod |
|
25
|
def from_kb_paths(cls, kb_paths: List[Path], provider_manager=None) -> "PlanningAgent": |
|
26
|
"""Create an agent from knowledge base paths.""" |
|
27
|
kb = KBContext() |
|
28
|
for path in kb_paths: |
|
29
|
kb.add_source(path) |
|
30
|
kb.load(provider_manager=provider_manager) |
|
31
|
|
|
32
|
context = AgentContext( |
|
33
|
knowledge_graph=kb.knowledge_graph, |
|
34
|
query_engine=kb.query_engine, |
|
35
|
provider_manager=provider_manager, |
|
36
|
) |
|
37
|
return cls(context) |
|
38
|
|
|
39
|
def execute(self, request: str) -> List[Artifact]: |
|
40
|
"""Execute a user request by selecting and running appropriate skills.""" |
|
41
|
# Step 1: Build context summary for LLM |
|
42
|
kb_summary = "" |
|
43
|
if self.context.query_engine: |
|
44
|
stats = self.context.query_engine.stats() |
|
45
|
kb_summary = stats.to_text() |
|
46
|
|
|
47
|
available_skills = list_skills() |
|
48
|
skill_descriptions = "\n".join(f"- {s.name}: {s.description}" for s in available_skills) |
|
49
|
|
|
50
|
# Step 2: Ask LLM to select skills |
|
51
|
plan_prompt = ( |
|
52
|
"You are a planning agent. Given a user request and available skills, " |
|
53
|
"select which skills to execute and in what order.\n\n" |
|
54
|
f"Knowledge base:\n{kb_summary}\n\n" |
|
55
|
f"Available skills:\n{skill_descriptions}\n\n" |
|
56
|
f"User request: {request}\n\n" |
|
57
|
"Return a JSON array of skill names to execute in order:\n" |
|
58
|
'[{"skill": "skill_name", "params": {}}]\n' |
|
59
|
"Return ONLY the JSON array." |
|
60
|
) |
|
61
|
|
|
62
|
if not self.context.provider_manager: |
|
63
|
# No LLM -- try to match skills by keyword |
|
64
|
return self._keyword_match_execute(request) |
|
65
|
|
|
66
|
raw = self.context.provider_manager.chat( |
|
67
|
[{"role": "user", "content": plan_prompt}], |
|
68
|
max_tokens=512, |
|
69
|
temperature=0.1, |
|
70
|
) |
|
71
|
|
|
72
|
from video_processor.utils.json_parsing import parse_json_from_response |
|
73
|
|
|
74
|
plan = parse_json_from_response(raw) |
|
75
|
|
|
76
|
artifacts = [] |
|
77
|
if isinstance(plan, list): |
|
78
|
for step in plan: |
|
79
|
if isinstance(step, dict) and "skill" in step: |
|
80
|
skill = get_skill(step["skill"]) |
|
81
|
if skill and skill.can_execute(self.context): |
|
82
|
params = step.get("params", {}) |
|
83
|
artifact = skill.execute(self.context, **params) |
|
84
|
artifacts.append(artifact) |
|
85
|
self.context.artifacts.append(artifact) |
|
86
|
|
|
87
|
return artifacts |
|
88
|
|
|
89
|
def _keyword_match_execute(self, request: str) -> List[Artifact]: |
|
90
|
"""Fallback: match skills by keywords in the request.""" |
|
91
|
request_lower = request.lower() |
|
92
|
artifacts = [] |
|
93
|
for skill in list_skills(): |
|
94
|
# Simple keyword matching |
|
95
|
skill_words = skill.name.replace("_", " ").split() |
|
96
|
if any(word in request_lower for word in skill_words): |
|
97
|
if skill.can_execute(self.context): |
|
98
|
artifact = skill.execute(self.context) |
|
99
|
artifacts.append(artifact) |
|
100
|
self.context.artifacts.append(artifact) |
|
101
|
return artifacts |
|
102
|
|
|
103
|
def chat(self, message: str) -> str: |
|
104
|
"""Interactive chat -- accumulate context and answer questions.""" |
|
105
|
self.context.conversation_history.append({"role": "user", "content": message}) |
|
106
|
|
|
107
|
if not self.context.provider_manager: |
|
108
|
return "Agent requires a configured LLM provider for chat mode." |
|
109
|
|
|
110
|
# Build system context |
|
111
|
kb_summary = "" |
|
112
|
if self.context.query_engine: |
|
113
|
stats = self.context.query_engine.stats() |
|
114
|
kb_summary = f"\n\nKnowledge base:\n{stats.to_text()}" |
|
115
|
|
|
116
|
artifacts_summary = "" |
|
117
|
if self.context.artifacts: |
|
118
|
artifacts_summary = "\n\nGenerated artifacts:\n" + "\n".join( |
|
119
|
f"- {a.name} ({a.artifact_type})" for a in self.context.artifacts |
|
120
|
) |
|
121
|
|
|
122
|
system_msg = ( |
|
123
|
"You are PlanOpticon, an AI planning companion built into the PlanOpticon CLI. " |
|
124
|
"PlanOpticon is a video analysis and knowledge extraction tool that processes " |
|
125
|
"recordings into structured knowledge graphs.\n\n" |
|
126
|
"You are running inside the interactive companion REPL. The user can use these " |
|
127
|
"built-in commands (suggest them when relevant):\n" |
|
128
|
" /status - Show workspace status (loaded KG, videos, docs)\n" |
|
129
|
" /entities [--type T] - List knowledge graph entities\n" |
|
130
|
" /search TERM - Search entities by name\n" |
|
131
|
" /neighbors ENTITY - Show entity relationships\n" |
|
132
|
" /export FORMAT - Export KG (markdown, obsidian, notion, csv)\n" |
|
133
|
" /analyze PATH - Analyze a video or document\n" |
|
134
|
" /ingest PATH - Ingest a file into the knowledge graph\n" |
|
135
|
" /auth SERVICE - Authenticate with a service " |
|
136
|
"(zoom, google, microsoft, notion, dropbox, github)\n" |
|
137
|
" /provider [NAME] - List or switch LLM provider\n" |
|
138
|
" /model [NAME] - Show or switch chat model\n" |
|
139
|
" /plan - Generate a project plan\n" |
|
140
|
" /prd - Generate a PRD\n" |
|
141
|
" /tasks - Generate a task breakdown\n\n" |
|
142
|
"PlanOpticon CLI commands the user can run outside the REPL:\n" |
|
143
|
" planopticon auth zoom|google|microsoft - Authenticate with cloud services\n" |
|
144
|
" planopticon recordings zoom-list|teams-list|meet-list - List cloud recordings\n" |
|
145
|
" planopticon analyze -i VIDEO - Analyze a video file\n" |
|
146
|
" planopticon query - Query the knowledge graph\n" |
|
147
|
" planopticon export FORMAT PATH - Export knowledge graph\n\n" |
|
148
|
f"{kb_summary}{artifacts_summary}\n\n" |
|
149
|
"Help the user with their planning tasks. When they ask about capabilities, " |
|
150
|
"refer them to the appropriate built-in commands. Ask clarifying questions " |
|
151
|
"to gather requirements. When ready, suggest using specific skills or commands " |
|
152
|
"to generate artifacts." |
|
153
|
) |
|
154
|
|
|
155
|
messages = [{"role": "system", "content": system_msg}] + self.context.conversation_history |
|
156
|
|
|
157
|
response = self.context.provider_manager.chat(messages, max_tokens=2048, temperature=0.5) |
|
158
|
self.context.conversation_history.append({"role": "assistant", "content": response}) |
|
159
|
return response |
|
160
|
|