kestrel-feature-reflection 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kestrel_feature_reflection/__init__.py +51 -0
- kestrel_feature_reflection/analyzer.py +440 -0
- kestrel_feature_reflection/approval.py +143 -0
- kestrel_feature_reflection/checks/__init__.py +7 -0
- kestrel_feature_reflection/checks/arms.py +534 -0
- kestrel_feature_reflection/checks/base.py +28 -0
- kestrel_feature_reflection/checks/memory.py +198 -0
- kestrel_feature_reflection/checks/mind.py +120 -0
- kestrel_feature_reflection/component.yaml +67 -0
- kestrel_feature_reflection/db_helpers.py +440 -0
- kestrel_feature_reflection/economics.py +107 -0
- kestrel_feature_reflection/feature.py +869 -0
- kestrel_feature_reflection/formatters.py +275 -0
- kestrel_feature_reflection/hooks.py +156 -0
- kestrel_feature_reflection/models.py +552 -0
- kestrel_feature_reflection/prioritizer.py +86 -0
- kestrel_feature_reflection/prompts.py +242 -0
- kestrel_feature_reflection/self_model.py +387 -0
- kestrel_feature_reflection/self_model_handler.py +160 -0
- kestrel_feature_reflection/ticket_creator.py +356 -0
- kestrel_feature_reflection/ticket_handler.py +203 -0
- kestrel_feature_reflection/training.py +184 -0
- kestrel_feature_reflection-0.1.0.dist-info/METADATA +35 -0
- kestrel_feature_reflection-0.1.0.dist-info/RECORD +27 -0
- kestrel_feature_reflection-0.1.0.dist-info/WHEEL +4 -0
- kestrel_feature_reflection-0.1.0.dist-info/entry_points.txt +2 -0
- kestrel_feature_reflection-0.1.0.dist-info/licenses/LICENSE +190 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Reflection Feature for agent self-improvement.
|
|
3
|
+
|
|
4
|
+
This feature enables Kestrel agents to:
|
|
5
|
+
- Reflect on past interactions
|
|
6
|
+
- Generate insights about performance
|
|
7
|
+
- Propose self-improvements
|
|
8
|
+
- Apply approved behavioral changes
|
|
9
|
+
- Create GitHub tickets from actionable insights
|
|
10
|
+
- Manage self-model in decentralized storage (Filecoin)
|
|
11
|
+
|
|
12
|
+
The feature integrates with the sleep cycle for automatic nightly reflection.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from .models import (
|
|
16
|
+
InsightType,
|
|
17
|
+
ChangeType,
|
|
18
|
+
Insight,
|
|
19
|
+
ReflectionSession,
|
|
20
|
+
ImprovementProposal,
|
|
21
|
+
BehaviorRule,
|
|
22
|
+
SelfModel,
|
|
23
|
+
)
|
|
24
|
+
from .analyzer import InteractionAnalyzer
|
|
25
|
+
from .feature import ReflectionFeature
|
|
26
|
+
from .economics import EconomicGate, ConfigurationError as EconomicsConfigError
|
|
27
|
+
from .ticket_creator import TicketCreator, ConfigurationError as TicketConfigError
|
|
28
|
+
from .self_model import SelfModelManager, ConfigurationError as SelfModelConfigError
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
# Models
|
|
32
|
+
"InsightType",
|
|
33
|
+
"ChangeType",
|
|
34
|
+
"Insight",
|
|
35
|
+
"ReflectionSession",
|
|
36
|
+
"ImprovementProposal",
|
|
37
|
+
"BehaviorRule",
|
|
38
|
+
"SelfModel",
|
|
39
|
+
# Core classes
|
|
40
|
+
"InteractionAnalyzer",
|
|
41
|
+
"ReflectionFeature",
|
|
42
|
+
# Economic gates
|
|
43
|
+
"EconomicGate",
|
|
44
|
+
"EconomicsConfigError",
|
|
45
|
+
# GitHub tickets
|
|
46
|
+
"TicketCreator",
|
|
47
|
+
"TicketConfigError",
|
|
48
|
+
# Self-model management
|
|
49
|
+
"SelfModelManager",
|
|
50
|
+
"SelfModelConfigError",
|
|
51
|
+
]
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interaction Analyzer for agent self-reflection.
|
|
3
|
+
|
|
4
|
+
Uses LLM to analyze past interactions and generate insights about:
|
|
5
|
+
- What worked well (successes)
|
|
6
|
+
- What could be improved (failures)
|
|
7
|
+
- Recurring patterns
|
|
8
|
+
- Actionable improvements
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
from datetime import datetime, timedelta
|
|
14
|
+
from typing import List, Optional, Dict, Any
|
|
15
|
+
|
|
16
|
+
from .models import Insight, InsightType
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Prompt for the LLM to analyze interactions
|
|
22
|
+
ANALYSIS_PROMPT_TEMPLATE = """You are performing self-reflection on your past interactions as a Kestrel agent.
|
|
23
|
+
|
|
24
|
+
Your goal is to honestly assess your performance and identify ways to improve.
|
|
25
|
+
|
|
26
|
+
## Recent Conversations
|
|
27
|
+
{interaction_summaries}
|
|
28
|
+
|
|
29
|
+
## Memory Episodes (Consolidated Memories)
|
|
30
|
+
{episode_summaries}
|
|
31
|
+
|
|
32
|
+
## Analysis Depth: {depth}
|
|
33
|
+
{depth_instructions}
|
|
34
|
+
|
|
35
|
+
## Questions to Consider
|
|
36
|
+
1. What did users seem satisfied with? What responses got positive engagement?
|
|
37
|
+
2. Where did I struggle or make mistakes? What went poorly?
|
|
38
|
+
3. Are there patterns in what users ask for? Recurring themes or needs?
|
|
39
|
+
4. What would make me more helpful? Specific improvements I could make?
|
|
40
|
+
5. Did I respect privacy and constitutional bounds? Any violations?
|
|
41
|
+
6. Were there times I was uncertain but didn't express it? Times I should have asked clarifying questions?
|
|
42
|
+
|
|
43
|
+
## Output Format
|
|
44
|
+
Respond with a JSON object containing your insights:
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{{
|
|
48
|
+
"insights": [
|
|
49
|
+
{{
|
|
50
|
+
"type": "pattern|improvement|success|failure|anomaly",
|
|
51
|
+
"title": "Brief descriptive title",
|
|
52
|
+
"description": "Detailed explanation of the insight",
|
|
53
|
+
"evidence": ["message_id_1", "message_id_2"],
|
|
54
|
+
"confidence": 0.8,
|
|
55
|
+
"actionable": true,
|
|
56
|
+
"suggested_action": "Specific action to take (if actionable)"
|
|
57
|
+
}}
|
|
58
|
+
]
|
|
59
|
+
}}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Guidelines
|
|
63
|
+
- Be honest and self-critical. The goal is genuine improvement.
|
|
64
|
+
- Each insight should be specific and grounded in evidence.
|
|
65
|
+
- Only mark insights as "actionable" if there's a clear action to take.
|
|
66
|
+
- Confidence should reflect how certain you are (0.0 = guess, 1.0 = certain).
|
|
67
|
+
- For improvements, suggest concrete changes, not vague aspirations.
|
|
68
|
+
|
|
69
|
+
Respond ONLY with the JSON object, no other text."""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
DEPTH_INSTRUCTIONS = {
|
|
73
|
+
"shallow": "Quick analysis - focus on obvious patterns and issues. 2-4 insights max.",
|
|
74
|
+
"normal": "Standard analysis - thorough review of interactions. 4-8 insights expected.",
|
|
75
|
+
"deep": "Deep analysis - examine subtle patterns, emotional dynamics, and implicit feedback. 6-12 insights expected.",
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class InteractionAnalyzer:
|
|
80
|
+
"""
|
|
81
|
+
Analyzes past interactions to generate insights.
|
|
82
|
+
|
|
83
|
+
Uses the LLM to perform self-reflection on conversation history
|
|
84
|
+
and memory episodes, producing structured insights.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __init__(
|
|
88
|
+
self,
|
|
89
|
+
llm_service,
|
|
90
|
+
conversation_store,
|
|
91
|
+
episode_store=None,
|
|
92
|
+
agent_id: str = "",
|
|
93
|
+
):
|
|
94
|
+
"""
|
|
95
|
+
Initialize the analyzer.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
llm_service: LLMService instance for generation
|
|
99
|
+
conversation_store: Store for conversation history
|
|
100
|
+
episode_store: Store for memory episodes (optional)
|
|
101
|
+
agent_id: Agent identifier for filtering
|
|
102
|
+
"""
|
|
103
|
+
self.llm = llm_service
|
|
104
|
+
self.conversations = conversation_store
|
|
105
|
+
self.episodes = episode_store
|
|
106
|
+
self.agent_id = agent_id
|
|
107
|
+
|
|
108
|
+
async def analyze(
|
|
109
|
+
self,
|
|
110
|
+
scope: str = "today",
|
|
111
|
+
depth: str = "normal",
|
|
112
|
+
max_interactions: int = 50,
|
|
113
|
+
max_episodes: int = 10,
|
|
114
|
+
) -> List[Insight]:
|
|
115
|
+
"""
|
|
116
|
+
Analyze interactions and generate insights.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
scope: Time range - 'session', 'today', 'week', 'month', 'all'
|
|
120
|
+
depth: Analysis depth - 'shallow', 'normal', 'deep'
|
|
121
|
+
max_interactions: Maximum interactions to analyze
|
|
122
|
+
max_episodes: Maximum episodes to include
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
List of Insight objects
|
|
126
|
+
"""
|
|
127
|
+
logger.info(f"Starting interaction analysis: scope={scope}, depth={depth}")
|
|
128
|
+
|
|
129
|
+
# Load interactions based on scope
|
|
130
|
+
interactions = await self._load_interactions(scope, max_interactions)
|
|
131
|
+
if not interactions:
|
|
132
|
+
logger.info("No interactions to analyze")
|
|
133
|
+
return []
|
|
134
|
+
|
|
135
|
+
# Load episodes if available
|
|
136
|
+
episodes = []
|
|
137
|
+
if self.episodes:
|
|
138
|
+
episodes = await self._load_episodes(scope, max_episodes)
|
|
139
|
+
|
|
140
|
+
# Build analysis prompt
|
|
141
|
+
prompt = self._build_prompt(interactions, episodes, depth)
|
|
142
|
+
|
|
143
|
+
# Get LLM analysis
|
|
144
|
+
try:
|
|
145
|
+
response = await self.llm.generate(
|
|
146
|
+
system_prompt="You are an AI assistant performing self-reflection analysis. Respond only with valid JSON.",
|
|
147
|
+
user_prompt=prompt,
|
|
148
|
+
force_local_only=False, # Use best available model
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Handle different response types
|
|
152
|
+
response_text = response if isinstance(response, str) else response.content
|
|
153
|
+
|
|
154
|
+
# Parse insights from response
|
|
155
|
+
insights = self._parse_insights(response_text)
|
|
156
|
+
logger.info(f"Generated {len(insights)} insights from analysis")
|
|
157
|
+
return insights
|
|
158
|
+
|
|
159
|
+
except Exception as e:
|
|
160
|
+
logger.error(f"Analysis failed: {e}")
|
|
161
|
+
return []
|
|
162
|
+
|
|
163
|
+
async def _load_interactions(
|
|
164
|
+
self,
|
|
165
|
+
scope: str,
|
|
166
|
+
max_count: int,
|
|
167
|
+
) -> List[Dict[str, Any]]:
|
|
168
|
+
"""Load interactions based on time scope."""
|
|
169
|
+
# Calculate time range
|
|
170
|
+
now = datetime.utcnow()
|
|
171
|
+
|
|
172
|
+
if scope == "session":
|
|
173
|
+
# Last 30 minutes
|
|
174
|
+
since = now - timedelta(minutes=30)
|
|
175
|
+
elif scope == "today":
|
|
176
|
+
# Today (from midnight)
|
|
177
|
+
since = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
178
|
+
elif scope == "week":
|
|
179
|
+
# Last 7 days
|
|
180
|
+
since = now - timedelta(days=7)
|
|
181
|
+
elif scope == "month":
|
|
182
|
+
# Last 30 days
|
|
183
|
+
since = now - timedelta(days=30)
|
|
184
|
+
else:
|
|
185
|
+
# All time - just use a large lookback
|
|
186
|
+
since = now - timedelta(days=365)
|
|
187
|
+
|
|
188
|
+
# Fetch from conversation store
|
|
189
|
+
try:
|
|
190
|
+
if hasattr(self.conversations, 'get_history_since'):
|
|
191
|
+
messages = await self.conversations.get_history_since(
|
|
192
|
+
since,
|
|
193
|
+
limit=max_count,
|
|
194
|
+
)
|
|
195
|
+
elif hasattr(self.conversations, 'get_conversation_history'):
|
|
196
|
+
# Fallback to getting recent history
|
|
197
|
+
# Note: AsyncConversationStore is already scoped by agent_id in constructor
|
|
198
|
+
messages = await self.conversations.get_conversation_history(
|
|
199
|
+
limit=max_count,
|
|
200
|
+
)
|
|
201
|
+
else:
|
|
202
|
+
logger.warning("Conversation store has no history retrieval method")
|
|
203
|
+
return []
|
|
204
|
+
|
|
205
|
+
# Convert to list of dicts if needed
|
|
206
|
+
interactions = []
|
|
207
|
+
for msg in messages:
|
|
208
|
+
if isinstance(msg, dict):
|
|
209
|
+
interactions.append(msg)
|
|
210
|
+
elif hasattr(msg, 'to_dict'):
|
|
211
|
+
interactions.append(msg.to_dict())
|
|
212
|
+
else:
|
|
213
|
+
interactions.append({
|
|
214
|
+
"role": getattr(msg, 'role', 'unknown'),
|
|
215
|
+
"content": getattr(msg, 'content', str(msg)),
|
|
216
|
+
"metadata": getattr(msg, 'metadata', {}),
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
return interactions
|
|
220
|
+
|
|
221
|
+
except Exception as e:
|
|
222
|
+
logger.error(f"Failed to load interactions: {e}")
|
|
223
|
+
return []
|
|
224
|
+
|
|
225
|
+
async def _load_episodes(
|
|
226
|
+
self,
|
|
227
|
+
scope: str,
|
|
228
|
+
max_count: int,
|
|
229
|
+
) -> List[Dict[str, Any]]:
|
|
230
|
+
"""Load memory episodes based on time scope."""
|
|
231
|
+
if not self.episodes:
|
|
232
|
+
return []
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
if hasattr(self.episodes, 'get_recent_episodes'):
|
|
236
|
+
episodes = await self.episodes.get_recent_episodes(
|
|
237
|
+
limit=max_count,
|
|
238
|
+
agent_id=self.agent_id,
|
|
239
|
+
)
|
|
240
|
+
elif hasattr(self.episodes, 'list_episodes'):
|
|
241
|
+
episodes = await self.episodes.list_episodes(limit=max_count)
|
|
242
|
+
else:
|
|
243
|
+
return []
|
|
244
|
+
|
|
245
|
+
# Convert to list of dicts if needed
|
|
246
|
+
result = []
|
|
247
|
+
for ep in episodes:
|
|
248
|
+
if isinstance(ep, dict):
|
|
249
|
+
result.append(ep)
|
|
250
|
+
elif hasattr(ep, 'to_dict'):
|
|
251
|
+
result.append(ep.to_dict())
|
|
252
|
+
else:
|
|
253
|
+
result.append({
|
|
254
|
+
"title": getattr(ep, 'title', 'Unknown'),
|
|
255
|
+
"summary": getattr(ep, 'summary', ''),
|
|
256
|
+
"emotional_arc": getattr(ep, 'emotional_arc', ''),
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
return result
|
|
260
|
+
|
|
261
|
+
except Exception as e:
|
|
262
|
+
logger.error(f"Failed to load episodes: {e}")
|
|
263
|
+
return []
|
|
264
|
+
|
|
265
|
+
def _build_prompt(
|
|
266
|
+
self,
|
|
267
|
+
interactions: List[Dict[str, Any]],
|
|
268
|
+
episodes: List[Dict[str, Any]],
|
|
269
|
+
depth: str,
|
|
270
|
+
) -> str:
|
|
271
|
+
"""Build the analysis prompt from interactions and episodes."""
|
|
272
|
+
# Summarize interactions
|
|
273
|
+
interaction_lines = []
|
|
274
|
+
for i, msg in enumerate(interactions[-50:], 1): # Limit to last 50
|
|
275
|
+
role = msg.get("role", "unknown")
|
|
276
|
+
content = msg.get("content", "")
|
|
277
|
+
# Truncate long messages
|
|
278
|
+
if len(content) > 500:
|
|
279
|
+
content = content[:500] + "..."
|
|
280
|
+
|
|
281
|
+
# Include metadata hints if available
|
|
282
|
+
metadata = msg.get("metadata", {})
|
|
283
|
+
emotional = ""
|
|
284
|
+
if metadata.get("emotional_valence"):
|
|
285
|
+
valence = metadata["emotional_valence"]
|
|
286
|
+
emotional = f" [emotion: {'positive' if valence > 0 else 'negative' if valence < 0 else 'neutral'}]"
|
|
287
|
+
|
|
288
|
+
msg_id = msg.get("id", f"msg_{i}")
|
|
289
|
+
interaction_lines.append(f"[{msg_id}] {role.upper()}{emotional}: {content}")
|
|
290
|
+
|
|
291
|
+
interaction_summary = "\n".join(interaction_lines) if interaction_lines else "(No recent interactions)"
|
|
292
|
+
|
|
293
|
+
# Summarize episodes
|
|
294
|
+
episode_lines = []
|
|
295
|
+
for ep in episodes:
|
|
296
|
+
title = ep.get("title", "Untitled")
|
|
297
|
+
summary = ep.get("summary", "")
|
|
298
|
+
arc = ep.get("emotional_arc", "")
|
|
299
|
+
episode_lines.append(f"- {title}: {summary[:200]}... (arc: {arc})")
|
|
300
|
+
|
|
301
|
+
episode_summary = "\n".join(episode_lines) if episode_lines else "(No memory episodes)"
|
|
302
|
+
|
|
303
|
+
# Get depth instructions
|
|
304
|
+
depth_inst = DEPTH_INSTRUCTIONS.get(depth, DEPTH_INSTRUCTIONS["normal"])
|
|
305
|
+
|
|
306
|
+
return ANALYSIS_PROMPT_TEMPLATE.format(
|
|
307
|
+
interaction_summaries=interaction_summary,
|
|
308
|
+
episode_summaries=episode_summary,
|
|
309
|
+
depth=depth,
|
|
310
|
+
depth_instructions=depth_inst,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
def _parse_insights(self, response_text: str) -> List[Insight]:
|
|
314
|
+
"""Parse LLM response into Insight objects.
|
|
315
|
+
|
|
316
|
+
Handles common local-model quirks:
|
|
317
|
+
- Markdown code fences around JSON
|
|
318
|
+
- Thinking/reasoning text before/after JSON
|
|
319
|
+
- Multiple JSON objects in response
|
|
320
|
+
"""
|
|
321
|
+
import uuid
|
|
322
|
+
import re
|
|
323
|
+
|
|
324
|
+
text = response_text.strip()
|
|
325
|
+
|
|
326
|
+
# Strategy 1: Extract JSON from markdown code fence
|
|
327
|
+
fence_match = re.search(r'```(?:json)?\s*\n?(.*?)\n?```', text, re.DOTALL)
|
|
328
|
+
if fence_match:
|
|
329
|
+
text = fence_match.group(1).strip()
|
|
330
|
+
|
|
331
|
+
# Strategy 2: Find the first complete JSON object with { ... }
|
|
332
|
+
if not text.startswith("{"):
|
|
333
|
+
brace_start = text.find("{")
|
|
334
|
+
if brace_start >= 0:
|
|
335
|
+
text = text[brace_start:]
|
|
336
|
+
|
|
337
|
+
# Strategy 3: Trim trailing non-JSON content after the closing brace
|
|
338
|
+
if text.startswith("{"):
|
|
339
|
+
depth = 0
|
|
340
|
+
in_string = False
|
|
341
|
+
escape_next = False
|
|
342
|
+
end_pos = -1
|
|
343
|
+
for i, ch in enumerate(text):
|
|
344
|
+
if escape_next:
|
|
345
|
+
escape_next = False
|
|
346
|
+
continue
|
|
347
|
+
if ch == '\\' and in_string:
|
|
348
|
+
escape_next = True
|
|
349
|
+
continue
|
|
350
|
+
if ch == '"' and not escape_next:
|
|
351
|
+
in_string = not in_string
|
|
352
|
+
continue
|
|
353
|
+
if in_string:
|
|
354
|
+
continue
|
|
355
|
+
if ch == '{':
|
|
356
|
+
depth += 1
|
|
357
|
+
elif ch == '}':
|
|
358
|
+
depth -= 1
|
|
359
|
+
if depth == 0:
|
|
360
|
+
end_pos = i + 1
|
|
361
|
+
break
|
|
362
|
+
if end_pos > 0:
|
|
363
|
+
text = text[:end_pos]
|
|
364
|
+
|
|
365
|
+
try:
|
|
366
|
+
data = json.loads(text)
|
|
367
|
+
except json.JSONDecodeError as e:
|
|
368
|
+
logger.error(f"Failed to parse JSON response: {e}")
|
|
369
|
+
logger.debug(f"Response text (first 500 chars): {response_text[:500]}")
|
|
370
|
+
return []
|
|
371
|
+
|
|
372
|
+
insights_data = data.get("insights", [])
|
|
373
|
+
if not insights_data:
|
|
374
|
+
logger.info(f"JSON parsed but no insights array (keys: {list(data.keys())})")
|
|
375
|
+
|
|
376
|
+
insights = []
|
|
377
|
+
for item in insights_data:
|
|
378
|
+
try:
|
|
379
|
+
insight = Insight(
|
|
380
|
+
id=str(uuid.uuid4()),
|
|
381
|
+
type=InsightType(item.get("type", "pattern")),
|
|
382
|
+
title=item.get("title", "Untitled Insight"),
|
|
383
|
+
description=item.get("description", ""),
|
|
384
|
+
evidence=item.get("evidence", []),
|
|
385
|
+
confidence=float(item.get("confidence", 0.5)),
|
|
386
|
+
actionable=bool(item.get("actionable", False)),
|
|
387
|
+
suggested_action=item.get("suggested_action"),
|
|
388
|
+
)
|
|
389
|
+
insights.append(insight)
|
|
390
|
+
except Exception as e:
|
|
391
|
+
logger.warning(f"Failed to parse insight: {e}")
|
|
392
|
+
continue
|
|
393
|
+
|
|
394
|
+
return insights
|
|
395
|
+
|
|
396
|
+
async def analyze_error(
|
|
397
|
+
self,
|
|
398
|
+
error_context: Dict[str, Any],
|
|
399
|
+
) -> List[Insight]:
|
|
400
|
+
"""
|
|
401
|
+
Analyze a specific error for learning.
|
|
402
|
+
|
|
403
|
+
Called when the agent encounters an error to learn from it.
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
error_context: Dict with error details (type, message, traceback, etc.)
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
List of Insight objects about the error
|
|
410
|
+
"""
|
|
411
|
+
error_prompt = f"""Analyze this error that occurred during agent operation:
|
|
412
|
+
|
|
413
|
+
## Error Details
|
|
414
|
+
- Type: {error_context.get('type', 'Unknown')}
|
|
415
|
+
- Message: {error_context.get('message', 'No message')}
|
|
416
|
+
- Context: {error_context.get('context', 'No context')}
|
|
417
|
+
|
|
418
|
+
## What Happened
|
|
419
|
+
{error_context.get('description', 'No description available')}
|
|
420
|
+
|
|
421
|
+
## Analysis Questions
|
|
422
|
+
1. What was the root cause?
|
|
423
|
+
2. How could this be prevented?
|
|
424
|
+
3. What should I do differently next time?
|
|
425
|
+
|
|
426
|
+
Respond with JSON containing insights about this error.
|
|
427
|
+
"""
|
|
428
|
+
|
|
429
|
+
try:
|
|
430
|
+
response = await self.llm.generate(
|
|
431
|
+
system_prompt="Analyze this error and provide learning insights as JSON.",
|
|
432
|
+
user_prompt=error_prompt,
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
response_text = response if isinstance(response, str) else response.content
|
|
436
|
+
return self._parse_insights(response_text)
|
|
437
|
+
|
|
438
|
+
except Exception as e:
|
|
439
|
+
logger.error(f"Error analysis failed: {e}")
|
|
440
|
+
return []
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Constitutional approval handling for reflection feature.
|
|
3
|
+
|
|
4
|
+
Manages approval requests and improvement application logic.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import uuid
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from kestrel_sovereign.kestrel_config.constants import APPROVAL_TIMEOUT_DEFAULT
|
|
13
|
+
|
|
14
|
+
from .models import ImprovementProposal, BehaviorRule, ChangeType
|
|
15
|
+
from .prompts import format_approval_prompt
|
|
16
|
+
from .db_helpers import ReflectionDatabaseHelper
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ApprovalHandler:
|
|
22
|
+
"""Handles constitutional approval for self-modifications."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, agent, db_helper: Optional[ReflectionDatabaseHelper] = None):
|
|
25
|
+
"""
|
|
26
|
+
Initialize the approval handler.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
agent: The agent instance
|
|
30
|
+
db_helper: Database helper for storing behavior rules
|
|
31
|
+
"""
|
|
32
|
+
self.agent = agent
|
|
33
|
+
self.db_helper = db_helper
|
|
34
|
+
|
|
35
|
+
async def request_approval(self, proposal: ImprovementProposal) -> bool:
|
|
36
|
+
"""
|
|
37
|
+
Request constitutional approval for self-modification.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
proposal: The improvement proposal to approve
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
True if approved, False otherwise
|
|
44
|
+
"""
|
|
45
|
+
# Check if we have access to security feature
|
|
46
|
+
security = self._get_security_feature()
|
|
47
|
+
|
|
48
|
+
if not security or not hasattr(security, 'approval_queue'):
|
|
49
|
+
logger.warning("SecurityFeature not available, auto-rejecting self-modification")
|
|
50
|
+
proposal.rejection_reason = "Security feature not available"
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
# Build approval request context using prompt formatter
|
|
54
|
+
context = format_approval_prompt(proposal)
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
approved, scope = await security.approval_queue.request_approval(
|
|
58
|
+
feature_name="reflection",
|
|
59
|
+
tool_name="self_modify",
|
|
60
|
+
tool_args={
|
|
61
|
+
"proposal_id": proposal.id,
|
|
62
|
+
"change_type": proposal.change_type.value,
|
|
63
|
+
"title": proposal.title,
|
|
64
|
+
"description": proposal.description[:500],
|
|
65
|
+
},
|
|
66
|
+
timeout=APPROVAL_TIMEOUT_DEFAULT, # 5 minutes
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
proposal.approved = approved
|
|
70
|
+
if approved:
|
|
71
|
+
proposal.approved_at = datetime.utcnow()
|
|
72
|
+
proposal.approved_by = "user"
|
|
73
|
+
else:
|
|
74
|
+
proposal.rejection_reason = "User denied self-modification"
|
|
75
|
+
|
|
76
|
+
return approved
|
|
77
|
+
|
|
78
|
+
except TimeoutError:
|
|
79
|
+
proposal.rejection_reason = "Approval request timed out"
|
|
80
|
+
return False
|
|
81
|
+
except Exception as e:
|
|
82
|
+
logger.error(f"Approval request failed: {e}")
|
|
83
|
+
proposal.rejection_reason = f"Approval error: {e}"
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
async def apply_improvement(self, proposal: ImprovementProposal) -> None:
|
|
87
|
+
"""
|
|
88
|
+
Apply an approved improvement.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
proposal: The approved improvement proposal
|
|
92
|
+
"""
|
|
93
|
+
if not proposal.approved:
|
|
94
|
+
raise ValueError("Cannot apply unapproved proposal")
|
|
95
|
+
|
|
96
|
+
if proposal.change_type == ChangeType.PROMPT:
|
|
97
|
+
# Add to agent's prompt additions
|
|
98
|
+
await self._add_prompt_guidance(proposal)
|
|
99
|
+
else:
|
|
100
|
+
# Store as behavioral rule
|
|
101
|
+
await self._add_behavior_rule(proposal)
|
|
102
|
+
|
|
103
|
+
logger.info(f"Applied improvement: {proposal.title}")
|
|
104
|
+
|
|
105
|
+
async def _add_prompt_guidance(self, proposal: ImprovementProposal) -> None:
|
|
106
|
+
"""Add guidance to the agent's prompt additions."""
|
|
107
|
+
# Store as a behavior rule with "always" trigger
|
|
108
|
+
rule = BehaviorRule(
|
|
109
|
+
id=str(uuid.uuid4()),
|
|
110
|
+
proposal_id=proposal.id,
|
|
111
|
+
trigger_condition="always",
|
|
112
|
+
action=proposal.proposed_change,
|
|
113
|
+
change_type=proposal.change_type,
|
|
114
|
+
priority=10, # Higher priority for prompt additions
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
if self.db_helper:
|
|
118
|
+
await self.db_helper.store_behavior_rule(rule)
|
|
119
|
+
|
|
120
|
+
async def _add_behavior_rule(self, proposal: ImprovementProposal) -> None:
|
|
121
|
+
"""Add a behavior rule based on the proposal."""
|
|
122
|
+
# Parse trigger condition from description if available
|
|
123
|
+
trigger = "contextual" # Default trigger
|
|
124
|
+
|
|
125
|
+
rule = BehaviorRule(
|
|
126
|
+
id=str(uuid.uuid4()),
|
|
127
|
+
proposal_id=proposal.id,
|
|
128
|
+
trigger_condition=trigger,
|
|
129
|
+
action=proposal.proposed_change,
|
|
130
|
+
change_type=proposal.change_type,
|
|
131
|
+
priority=5,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if self.db_helper:
|
|
135
|
+
await self.db_helper.store_behavior_rule(rule)
|
|
136
|
+
|
|
137
|
+
def _get_security_feature(self):
|
|
138
|
+
"""Get the security feature from the agent."""
|
|
139
|
+
if hasattr(self.agent, 'get_feature'):
|
|
140
|
+
return self.agent.get_feature("security")
|
|
141
|
+
elif hasattr(self.agent, 'features'):
|
|
142
|
+
return self.agent.features.get("security")
|
|
143
|
+
return None
|