chuk-ai-session-manager 0.7.1__py3-none-any.whl → 0.8.1__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.
Files changed (46) hide show
  1. chuk_ai_session_manager/__init__.py +84 -40
  2. chuk_ai_session_manager/api/__init__.py +1 -1
  3. chuk_ai_session_manager/api/simple_api.py +53 -59
  4. chuk_ai_session_manager/exceptions.py +31 -17
  5. chuk_ai_session_manager/guards/__init__.py +118 -0
  6. chuk_ai_session_manager/guards/bindings.py +217 -0
  7. chuk_ai_session_manager/guards/cache.py +163 -0
  8. chuk_ai_session_manager/guards/manager.py +819 -0
  9. chuk_ai_session_manager/guards/models.py +498 -0
  10. chuk_ai_session_manager/guards/ungrounded.py +159 -0
  11. chuk_ai_session_manager/infinite_conversation.py +86 -79
  12. chuk_ai_session_manager/memory/__init__.py +247 -0
  13. chuk_ai_session_manager/memory/artifacts_bridge.py +469 -0
  14. chuk_ai_session_manager/memory/context_packer.py +347 -0
  15. chuk_ai_session_manager/memory/fault_handler.py +507 -0
  16. chuk_ai_session_manager/memory/manifest.py +307 -0
  17. chuk_ai_session_manager/memory/models.py +1084 -0
  18. chuk_ai_session_manager/memory/mutation_log.py +186 -0
  19. chuk_ai_session_manager/memory/pack_cache.py +206 -0
  20. chuk_ai_session_manager/memory/page_table.py +275 -0
  21. chuk_ai_session_manager/memory/prefetcher.py +192 -0
  22. chuk_ai_session_manager/memory/tlb.py +247 -0
  23. chuk_ai_session_manager/memory/vm_prompts.py +238 -0
  24. chuk_ai_session_manager/memory/working_set.py +574 -0
  25. chuk_ai_session_manager/models/__init__.py +21 -9
  26. chuk_ai_session_manager/models/event_source.py +3 -1
  27. chuk_ai_session_manager/models/event_type.py +10 -1
  28. chuk_ai_session_manager/models/session.py +103 -68
  29. chuk_ai_session_manager/models/session_event.py +69 -68
  30. chuk_ai_session_manager/models/session_metadata.py +9 -10
  31. chuk_ai_session_manager/models/session_run.py +21 -22
  32. chuk_ai_session_manager/models/token_usage.py +76 -76
  33. chuk_ai_session_manager/procedural_memory/__init__.py +70 -0
  34. chuk_ai_session_manager/procedural_memory/formatter.py +407 -0
  35. chuk_ai_session_manager/procedural_memory/manager.py +523 -0
  36. chuk_ai_session_manager/procedural_memory/models.py +371 -0
  37. chuk_ai_session_manager/sample_tools.py +79 -46
  38. chuk_ai_session_manager/session_aware_tool_processor.py +27 -16
  39. chuk_ai_session_manager/session_manager.py +259 -232
  40. chuk_ai_session_manager/session_prompt_builder.py +163 -111
  41. chuk_ai_session_manager/session_storage.py +45 -52
  42. {chuk_ai_session_manager-0.7.1.dist-info → chuk_ai_session_manager-0.8.1.dist-info}/METADATA +80 -4
  43. chuk_ai_session_manager-0.8.1.dist-info/RECORD +45 -0
  44. {chuk_ai_session_manager-0.7.1.dist-info → chuk_ai_session_manager-0.8.1.dist-info}/WHEEL +1 -1
  45. chuk_ai_session_manager-0.7.1.dist-info/RECORD +0 -22
  46. {chuk_ai_session_manager-0.7.1.dist-info → chuk_ai_session_manager-0.8.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,371 @@
1
+ # chuk_ai_session_manager/procedural_memory/models.py
2
+ """
3
+ Data models for procedural memory.
4
+
5
+ These models represent:
6
+ - Individual tool invocations (traces)
7
+ - Aggregated tool patterns (recipes)
8
+ - Fix relationships (how failures were resolved)
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from datetime import datetime, timezone
14
+ from enum import Enum
15
+ from typing import Any, Optional
16
+
17
+ from pydantic import BaseModel, Field
18
+
19
+
20
+ class ToolOutcome(str, Enum):
21
+ """Outcome of a tool invocation."""
22
+
23
+ SUCCESS = "success"
24
+ FAILURE = "failure"
25
+ PARTIAL = "partial" # Completed but with warnings/limitations
26
+ TIMEOUT = "timeout"
27
+ CANCELLED = "cancelled"
28
+
29
+
30
+ class ToolFixRelation(BaseModel):
31
+ """Describes how one call fixed a prior failure."""
32
+
33
+ failed_call_id: str
34
+ success_call_id: str
35
+ delta_args: dict[str, Any] = Field(default_factory=dict)
36
+ # delta_args structure:
37
+ # {
38
+ # "added": {"key": value, ...},
39
+ # "removed": ["key1", "key2"],
40
+ # "changed": {"key": {"from": old, "to": new}}
41
+ # }
42
+
43
+
44
+ class ToolLogEntry(BaseModel):
45
+ """
46
+ Single tool invocation record (the 'trace').
47
+
48
+ This captures everything about a single tool call:
49
+ - What was called and with what arguments
50
+ - The context/goal at the time
51
+ - The outcome and any errors
52
+ - Links to fixes if this was a failure that got resolved
53
+ """
54
+
55
+ # Identity
56
+ id: str # call-001, call-002, ...
57
+ timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
58
+
59
+ # Tool call details
60
+ tool_name: str
61
+ arguments: dict[str, Any] = Field(default_factory=dict)
62
+ arguments_hash: str = "" # For quick comparison
63
+
64
+ # Context at call time
65
+ context_goal: Optional[str] = None # What was user trying to do?
66
+ preceding_call_id: Optional[str] = None # Chain of calls in this turn
67
+
68
+ # Outcome
69
+ outcome: ToolOutcome
70
+ result_summary: str # Compact summary, not full result
71
+ result_type: Optional[str] = None # "sat", "list", "object", etc.
72
+ execution_time_ms: Optional[int] = None
73
+
74
+ # Error details (if failure)
75
+ error_type: Optional[str] = None # "unsat", "timeout", "validation", etc.
76
+ error_message: Optional[str] = None
77
+
78
+ # Fix tracking
79
+ fixed_by: Optional[str] = None # ID of call that fixed this failure
80
+ fix_for: Optional[str] = None # ID of call this fixed
81
+ delta_args: Optional[dict[str, Any]] = None # What changed to fix it
82
+
83
+ model_config = {"frozen": False}
84
+
85
+ def is_failure(self) -> bool:
86
+ """Check if this was a failed call."""
87
+ return self.outcome in (ToolOutcome.FAILURE, ToolOutcome.TIMEOUT)
88
+
89
+ def is_success(self) -> bool:
90
+ """Check if this was a successful call."""
91
+ return self.outcome == ToolOutcome.SUCCESS
92
+
93
+ def was_fixed(self) -> bool:
94
+ """Check if this failure was subsequently fixed."""
95
+ return self.fixed_by is not None
96
+
97
+ def is_fix(self) -> bool:
98
+ """Check if this call fixed a prior failure."""
99
+ return self.fix_for is not None
100
+
101
+ def format_compact(self) -> str:
102
+ """Format as compact string for logging."""
103
+ status = "✓" if self.is_success() else "✗"
104
+ return f"[{status}] {self.tool_name}: {self.result_summary}"
105
+
106
+ def format_for_context(self, include_args: bool = False) -> str:
107
+ """Format for injection into model context."""
108
+ status = "✓" if self.is_success() else "✗"
109
+ lines = [f"[{status}] {self.result_summary}"]
110
+
111
+ if include_args and self.arguments:
112
+ # Show abbreviated args
113
+ args_str = ", ".join(f"{k}={_abbrev(v)}" for k, v in self.arguments.items())
114
+ lines.append(f" args: {args_str}")
115
+
116
+ if self.is_fix() and self.delta_args:
117
+ lines.append(f" (fixed prior failure by: {self.delta_args})")
118
+
119
+ if self.is_failure() and self.was_fixed():
120
+ lines.append(f" (later fixed by {self.fixed_by})")
121
+
122
+ return "\n".join(lines)
123
+
124
+
125
+ class ErrorPattern(BaseModel):
126
+ """A pattern of errors for a tool."""
127
+
128
+ error_type: str
129
+ count: int = 1
130
+ contexts: list[str] = Field(
131
+ default_factory=list
132
+ ) # Goal contexts where this occurred
133
+ example_args: Optional[dict[str, Any]] = None
134
+ typical_fix: Optional[str] = None # Description of how it's usually fixed
135
+ fix_delta: Optional[dict[str, Any]] = None # Actual arg changes that fixed it
136
+
137
+
138
+ class SuccessPattern(BaseModel):
139
+ """A pattern of successful usage for a tool."""
140
+
141
+ goal_match: Optional[str] = None # What kind of goal this pattern works for
142
+ arg_hints: dict[str, Any] = Field(default_factory=dict)
143
+ # arg_hints: {"must_include": [...], "typical_values": {...}}
144
+ notes: Optional[str] = None
145
+ example_call_id: Optional[str] = None
146
+ delta_that_fixed: Optional[dict[str, Any]] = None # If this was a fix
147
+
148
+
149
+ class ToolPattern(BaseModel):
150
+ """
151
+ Aggregated knowledge about a tool (the 'recipe').
152
+
153
+ This accumulates across calls to learn:
154
+ - What arguments typically work
155
+ - What errors occur and how to fix them
156
+ - Success patterns for different goals
157
+ """
158
+
159
+ tool_name: str
160
+
161
+ # Statistics
162
+ total_calls: int = 0
163
+ success_count: int = 0
164
+ failure_count: int = 0
165
+
166
+ @property
167
+ def success_rate(self) -> float:
168
+ """Calculate success rate."""
169
+ if self.total_calls == 0:
170
+ return 0.0
171
+ return self.success_count / self.total_calls
172
+
173
+ # Argument statistics
174
+ common_args: dict[str, list[Any]] = Field(default_factory=dict)
175
+ # common_args: {"param_name": [most_common_values]}
176
+
177
+ # Success patterns
178
+ success_patterns: list[SuccessPattern] = Field(default_factory=list)
179
+
180
+ # Error patterns
181
+ error_patterns: list[ErrorPattern] = Field(default_factory=list)
182
+
183
+ # Timing
184
+ avg_execution_ms: Optional[float] = None
185
+ updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
186
+
187
+ model_config = {"frozen": False}
188
+
189
+ def record_call(self, entry: ToolLogEntry) -> None:
190
+ """Update pattern statistics from a call."""
191
+ self.total_calls += 1
192
+
193
+ if entry.is_success():
194
+ self.success_count += 1
195
+ elif entry.is_failure():
196
+ self.failure_count += 1
197
+
198
+ # Update timing
199
+ if entry.execution_time_ms:
200
+ if self.avg_execution_ms is None:
201
+ self.avg_execution_ms = float(entry.execution_time_ms)
202
+ else:
203
+ # Running average
204
+ self.avg_execution_ms = (
205
+ self.avg_execution_ms * (self.total_calls - 1)
206
+ + entry.execution_time_ms
207
+ ) / self.total_calls
208
+
209
+ # Track common args
210
+ for key, value in entry.arguments.items():
211
+ if key not in self.common_args:
212
+ self.common_args[key] = []
213
+ # Keep last few values (dedup)
214
+ if value not in self.common_args[key]:
215
+ self.common_args[key].append(value)
216
+ if len(self.common_args[key]) > 5:
217
+ self.common_args[key].pop(0)
218
+
219
+ self.updated_at = datetime.now(timezone.utc)
220
+
221
+ def add_error_pattern(
222
+ self,
223
+ error_type: str,
224
+ context: Optional[str] = None,
225
+ example_args: Optional[dict] = None,
226
+ ) -> ErrorPattern:
227
+ """Add or update an error pattern."""
228
+ # Find existing
229
+ for pattern in self.error_patterns:
230
+ if pattern.error_type == error_type:
231
+ pattern.count += 1
232
+ if context and context not in pattern.contexts:
233
+ pattern.contexts.append(context)
234
+ if len(pattern.contexts) > 5:
235
+ pattern.contexts.pop(0)
236
+ return pattern
237
+
238
+ # Create new
239
+ pattern = ErrorPattern(
240
+ error_type=error_type,
241
+ contexts=[context] if context else [],
242
+ example_args=example_args,
243
+ )
244
+ self.error_patterns.append(pattern)
245
+ return pattern
246
+
247
+ def add_success_pattern(
248
+ self,
249
+ goal_match: Optional[str] = None,
250
+ arg_hints: Optional[dict] = None,
251
+ delta_that_fixed: Optional[dict] = None,
252
+ example_call_id: Optional[str] = None,
253
+ ) -> SuccessPattern:
254
+ """Add a success pattern."""
255
+ pattern = SuccessPattern(
256
+ goal_match=goal_match,
257
+ arg_hints=arg_hints or {},
258
+ delta_that_fixed=delta_that_fixed,
259
+ example_call_id=example_call_id,
260
+ )
261
+ self.success_patterns.append(pattern)
262
+
263
+ # Keep limited history
264
+ if len(self.success_patterns) > 10:
265
+ self.success_patterns.pop(0)
266
+
267
+ return pattern
268
+
269
+ def record_fix(self, error_type: str, fix_delta: dict[str, Any]) -> None:
270
+ """Record that an error was fixed with specific arg changes."""
271
+ for pattern in self.error_patterns:
272
+ if pattern.error_type == error_type:
273
+ pattern.fix_delta = fix_delta
274
+ pattern.typical_fix = _describe_fix(fix_delta)
275
+ return
276
+
277
+ def format_for_context(self, max_errors: int = 3, max_successes: int = 3) -> str:
278
+ """Format pattern for injection into model context."""
279
+ lines = []
280
+
281
+ # Stats summary
282
+ lines.append(
283
+ f"Success rate: {self.success_rate:.0%} ({self.total_calls} calls)"
284
+ )
285
+
286
+ # Error patterns
287
+ if self.error_patterns:
288
+ lines.append("Common errors:")
289
+ for ep in self.error_patterns[-max_errors:]:
290
+ fix_hint = f" -> {ep.typical_fix}" if ep.typical_fix else ""
291
+ lines.append(f" - {ep.error_type} (x{ep.count}){fix_hint}")
292
+
293
+ # Success hints
294
+ if self.success_patterns:
295
+ lines.append("Success hints:")
296
+ for sp in self.success_patterns[-max_successes:]:
297
+ if sp.delta_that_fixed:
298
+ lines.append(f" - Fixed by: {sp.delta_that_fixed}")
299
+ elif sp.arg_hints:
300
+ lines.append(f" - Args: {sp.arg_hints}")
301
+
302
+ return "\n".join(lines)
303
+
304
+
305
+ class ProceduralMemory(BaseModel):
306
+ """
307
+ Container for all procedural memory state.
308
+
309
+ This is the "L2/L3" memory that persists beyond the hot cache.
310
+ """
311
+
312
+ session_id: str
313
+
314
+ # L2: Session tool log (append-only trace)
315
+ tool_log: list[ToolLogEntry] = Field(default_factory=list)
316
+ next_call_id: int = 1
317
+
318
+ # L3: Per-tool patterns (aggregated knowledge)
319
+ tool_patterns: dict[str, ToolPattern] = Field(default_factory=dict)
320
+
321
+ # Fix relationships
322
+ fix_relations: list[ToolFixRelation] = Field(default_factory=list)
323
+
324
+ # Metadata
325
+ created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
326
+ updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
327
+
328
+ model_config = {"frozen": False}
329
+
330
+ def get_pattern(self, tool_name: str) -> ToolPattern:
331
+ """Get or create pattern for a tool."""
332
+ if tool_name not in self.tool_patterns:
333
+ self.tool_patterns[tool_name] = ToolPattern(tool_name=tool_name)
334
+ return self.tool_patterns[tool_name]
335
+
336
+ def allocate_call_id(self) -> str:
337
+ """Allocate next call ID."""
338
+ call_id = f"call-{self.next_call_id:04d}"
339
+ self.next_call_id += 1
340
+ return call_id
341
+
342
+
343
+ # --- Helpers ---
344
+
345
+
346
+ def _abbrev(value: Any, max_len: int = 20) -> str:
347
+ """Abbreviate a value for display."""
348
+ s = str(value)
349
+ if len(s) > max_len:
350
+ return s[: max_len - 3] + "..."
351
+ return s
352
+
353
+
354
+ def _describe_fix(delta: dict[str, Any]) -> str:
355
+ """Generate human-readable description of a fix."""
356
+ parts = []
357
+
358
+ if "added" in delta:
359
+ added_keys = list(delta["added"].keys())
360
+ parts.append(f"add {', '.join(added_keys)}")
361
+
362
+ if "removed" in delta:
363
+ parts.append(f"remove {', '.join(delta['removed'])}")
364
+
365
+ if "changed" in delta:
366
+ changed = []
367
+ for k, v in delta["changed"].items():
368
+ changed.append(f"{k}: {_abbrev(v['from'])} -> {_abbrev(v['to'])}")
369
+ parts.append(f"change {'; '.join(changed)}")
370
+
371
+ return "; ".join(parts) if parts else "unknown changes"
@@ -11,24 +11,28 @@ from typing import Dict, Any
11
11
  from chuk_tool_processor.registry import register_tool
12
12
 
13
13
 
14
- @register_tool(name="calculator", namespace="default", description="Perform basic arithmetic operations")
14
+ @register_tool(
15
+ name="calculator",
16
+ namespace="default",
17
+ description="Perform basic arithmetic operations",
18
+ )
15
19
  class CalculatorTool:
16
20
  """Calculator tool for basic arithmetic."""
17
-
21
+
18
22
  async def execute(self, operation: str, a: float, b: float) -> Dict[str, Any]:
19
23
  """
20
24
  Perform a basic arithmetic operation.
21
-
25
+
22
26
  Args:
23
27
  operation: One of "add", "subtract", "multiply", "divide"
24
28
  a: First operand
25
29
  b: Second operand
26
-
30
+
27
31
  Returns:
28
32
  Dictionary with the result
29
33
  """
30
34
  print(f"🧮 Calculator executing: {a} {operation} {b}")
31
-
35
+
32
36
  if operation == "add":
33
37
  result = a + b
34
38
  elif operation == "subtract":
@@ -41,60 +45,84 @@ class CalculatorTool:
41
45
  result = a / b
42
46
  else:
43
47
  raise ValueError(f"Unknown operation: {operation}")
44
-
48
+
45
49
  return {
46
50
  "operation": operation,
47
51
  "a": a,
48
52
  "b": b,
49
53
  "result": result,
50
- "timestamp": datetime.now().isoformat()
54
+ "timestamp": datetime.now().isoformat(),
51
55
  }
52
56
 
53
57
 
54
- @register_tool(name="weather", namespace="default", description="Get current weather information for a location")
58
+ @register_tool(
59
+ name="weather",
60
+ namespace="default",
61
+ description="Get current weather information for a location",
62
+ )
55
63
  class WeatherTool:
56
64
  """Weather tool that returns mock weather data."""
57
-
65
+
58
66
  async def execute(self, location: str) -> Dict[str, Any]:
59
67
  """
60
68
  Get weather information for a specific location.
61
-
69
+
62
70
  Args:
63
71
  location: The city or location to get weather for
64
-
72
+
65
73
  Returns:
66
74
  Dictionary with weather information
67
75
  """
68
76
  print(f"🌤️ Weather tool executing for: {location}")
69
77
  await asyncio.sleep(0.1) # Simulate API delay
70
-
78
+
71
79
  # Mock realistic weather based on location
72
80
  base_temp = 15 # Default moderate temperature
73
- if any(city in location.lower() for city in ["miami", "phoenix", "dubai", "singapore"]):
81
+ if any(
82
+ city in location.lower()
83
+ for city in ["miami", "phoenix", "dubai", "singapore"]
84
+ ):
74
85
  base_temp = 28
75
- elif any(city in location.lower() for city in ["moscow", "montreal", "oslo", "anchorage"]):
86
+ elif any(
87
+ city in location.lower()
88
+ for city in ["moscow", "montreal", "oslo", "anchorage"]
89
+ ):
76
90
  base_temp = -5
77
- elif any(city in location.lower() for city in ["london", "seattle", "vancouver"]):
91
+ elif any(
92
+ city in location.lower() for city in ["london", "seattle", "vancouver"]
93
+ ):
78
94
  base_temp = 12
79
- elif any(city in location.lower() for city in ["tokyo", "new york", "paris", "berlin"]):
95
+ elif any(
96
+ city in location.lower()
97
+ for city in ["tokyo", "new york", "paris", "berlin"]
98
+ ):
80
99
  base_temp = 18
81
-
100
+
82
101
  # Add some randomness
83
102
  temperature = base_temp + random.randint(-8, 12)
84
- conditions = ["Sunny", "Partly Cloudy", "Cloudy", "Light Rain", "Heavy Rain", "Snow", "Thunderstorm", "Foggy"]
103
+ conditions = [
104
+ "Sunny",
105
+ "Partly Cloudy",
106
+ "Cloudy",
107
+ "Light Rain",
108
+ "Heavy Rain",
109
+ "Snow",
110
+ "Thunderstorm",
111
+ "Foggy",
112
+ ]
85
113
  condition = random.choice(conditions)
86
114
  humidity = random.randint(35, 85)
87
115
  wind_speed = random.uniform(2.0, 25.0)
88
116
  feels_like = temperature + random.randint(-3, 3)
89
-
117
+
90
118
  # Adjust conditions based on temperature
91
119
  if temperature < 0:
92
120
  condition = random.choice(["Snow", "Cloudy", "Partly Cloudy"])
93
121
  elif temperature > 30:
94
122
  condition = random.choice(["Sunny", "Partly Cloudy", "Hot"])
95
-
123
+
96
124
  description = f"Current weather in {location} is {condition.lower()} with temperature {temperature}°C"
97
-
125
+
98
126
  return {
99
127
  "location": location,
100
128
  "temperature": float(temperature),
@@ -103,62 +131,65 @@ class WeatherTool:
103
131
  "wind_speed": round(wind_speed, 1),
104
132
  "description": description,
105
133
  "feels_like": float(feels_like),
106
- "timestamp": datetime.now().isoformat()
134
+ "timestamp": datetime.now().isoformat(),
107
135
  }
108
136
 
109
137
 
110
- @register_tool(name="search", namespace="default", description="Search for information on the internet")
138
+ @register_tool(
139
+ name="search",
140
+ namespace="default",
141
+ description="Search for information on the internet",
142
+ )
111
143
  class SearchTool:
112
144
  """Search tool that returns mock search results."""
113
-
145
+
114
146
  async def execute(self, query: str, max_results: int = 3) -> Dict[str, Any]:
115
147
  """
116
148
  Search for information on the internet.
117
-
149
+
118
150
  Args:
119
151
  query: Search query
120
152
  max_results: Maximum number of results to return
121
-
153
+
122
154
  Returns:
123
155
  Dictionary with search results
124
156
  """
125
157
  print(f"🔍 Search tool executing for: {query}")
126
158
  await asyncio.sleep(0.2) # Simulate API delay
127
-
128
- results = []
159
+
129
160
  query_lower = query.lower()
130
-
161
+
131
162
  # Generate contextually relevant mock results based on query
132
163
  if "climate" in query_lower or "environment" in query_lower:
133
164
  result_templates = [
134
165
  {
135
166
  "title": "Climate Change Adaptation Strategies - IPCC Report",
136
167
  "url": "https://www.ipcc.ch/adaptation-strategies",
137
- "snippet": "Comprehensive guide to climate change adaptation strategies for communities, businesses, and governments. Includes resilience planning and risk assessment."
168
+ "snippet": "Comprehensive guide to climate change adaptation strategies for communities, businesses, and governments. Includes resilience planning and risk assessment.",
138
169
  },
139
170
  {
140
- "title": "Environmental Adaptation Solutions | Climate.gov",
171
+ "title": "Environmental Adaptation Solutions | Climate.gov",
141
172
  "url": "https://www.climate.gov/adaptation-solutions",
142
- "snippet": "Evidence-based climate adaptation solutions including infrastructure improvements, ecosystem restoration, and community planning approaches."
173
+ "snippet": "Evidence-based climate adaptation solutions including infrastructure improvements, ecosystem restoration, and community planning approaches.",
143
174
  },
144
175
  {
145
176
  "title": "Building Climate Resilience: A Practical Guide",
146
- "url": "https://www.resilience.org/climate-guide",
147
- "snippet": "Practical steps for building climate resilience in your community. Covers early warning systems, green infrastructure, and adaptation planning."
148
- }
177
+ "url": "https://www.resilience.org/climate-guide",
178
+ "snippet": "Practical steps for building climate resilience in your community. Covers early warning systems, green infrastructure, and adaptation planning.",
179
+ },
149
180
  ]
150
181
  elif "weather" in query_lower:
151
182
  result_templates = [
152
183
  {
153
184
  "title": "Weather Forecast and Current Conditions",
154
185
  "url": "https://weather.com/forecast",
155
- "snippet": "Get accurate weather forecasts, current conditions, and severe weather alerts for your location."
186
+ "snippet": "Get accurate weather forecasts, current conditions, and severe weather alerts for your location.",
156
187
  },
157
188
  {
158
189
  "title": "Climate and Weather Patterns Explained",
159
190
  "url": "https://www.weatherpatterns.org",
160
- "snippet": "Understanding weather patterns, climate systems, and meteorological phenomena that affect daily weather."
161
- }
191
+ "snippet": "Understanding weather patterns, climate systems, and meteorological phenomena that affect daily weather.",
192
+ },
162
193
  ]
163
194
  else:
164
195
  # Generic results for other queries
@@ -166,29 +197,31 @@ class SearchTool:
166
197
  {
167
198
  "title": f"Everything You Need to Know About {query.title()}",
168
199
  "url": f"https://encyclopedia.com/{query.lower().replace(' ', '-')}",
169
- "snippet": f"Comprehensive information and resources about {query}. Expert insights, latest research, and practical applications."
200
+ "snippet": f"Comprehensive information and resources about {query}. Expert insights, latest research, and practical applications.",
170
201
  },
171
202
  {
172
203
  "title": f"{query.title()} - Latest News and Updates",
173
204
  "url": f"https://news.example.com/{query.lower().replace(' ', '-')}",
174
- "snippet": f"Stay up to date with the latest news, trends, and developments related to {query}."
205
+ "snippet": f"Stay up to date with the latest news, trends, and developments related to {query}.",
175
206
  },
176
207
  {
177
208
  "title": f"Guide to {query.title()} - Best Practices",
178
209
  "url": f"https://guides.com/{query.lower().replace(' ', '-')}",
179
- "snippet": f"Expert guide covering best practices, tips, and strategies for {query}. Includes real-world examples and case studies."
180
- }
210
+ "snippet": f"Expert guide covering best practices, tips, and strategies for {query}. Includes real-world examples and case studies.",
211
+ },
181
212
  ]
182
-
213
+
183
214
  # Select results up to max_results
184
215
  selected_results = result_templates[:max_results]
185
-
216
+
186
217
  return {
187
218
  "query": query,
188
219
  "results_count": len(selected_results),
189
220
  "results": selected_results,
190
- "timestamp": datetime.now().isoformat()
221
+ "timestamp": datetime.now().isoformat(),
191
222
  }
192
223
 
193
224
 
194
- print("✅ sample_tools.py: 3 tools defined with @register_tool decorator (corrected version)")
225
+ print(
226
+ "✅ sample_tools.py: 3 tools defined with @register_tool decorator (corrected version)"
227
+ )