Skip to content

Commit 0464e26

Browse files
Merge pull request #175 from BrainBlend-AI/feature/173-add-instructor-hooks
Add support for https://python.useinstructor.com/concepts/hooks/ - fixing #173
2 parents c456b8e + 12db8dc commit 0464e26

File tree

9 files changed

+3441
-3
lines changed

9 files changed

+3441
-3
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ For full, runnable examples, please refer to the following files in the `atomic-
276276

277277
In addition to the quickstart examples, we have more complex examples demonstrating the power of Atomic Agents:
278278

279+
- [Hooks System](/atomic-examples/hooks-example/README.md): Comprehensive demonstration of the AtomicAgent hook system for monitoring, error handling, and performance metrics with intelligent retry mechanisms.
279280
- [Basic Multimodal](/atomic-examples/basic-multimodal/README.md): Demonstrates how to analyze images with text, focusing on extracting structured information from nutrition labels using GPT-4 Vision capabilities.
280281
- [Deep Research](/atomic-examples/deep-research/README.md): An advanced example showing how to perform deep research tasks.
281282
- [Orchestration Agent](/atomic-examples/orchestration-agent/README.md): Shows how to create an Orchestrator Agent that intelligently decides between using different tools (search or calculator) based on user input.

atomic-agents/atomic_agents/agents/atomic_agent.py

Lines changed: 126 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import instructor
22
from pydantic import BaseModel, Field
3-
from typing import Optional, Type, Generator, AsyncGenerator, get_args
3+
from typing import Optional, Type, Generator, AsyncGenerator, get_args, Dict, List, Callable
4+
import logging
45
from atomic_agents.context.chat_history import ChatHistory
56
from atomic_agents.context.system_prompt_generator import (
67
BaseDynamicContextProvider,
@@ -73,10 +74,11 @@ class AgentConfig(BaseModel):
7374

7475
class AtomicAgent[InputSchema: BaseIOSchema, OutputSchema: BaseIOSchema]:
7576
"""
76-
Base class for chat agents.
77+
Base class for chat agents with full Instructor hook system integration.
7778
7879
This class provides the core functionality for handling chat interactions, including managing history,
79-
generating system prompts, and obtaining responses from a language model.
80+
generating system prompts, and obtaining responses from a language model. It includes comprehensive
81+
hook system support for monitoring and error handling.
8082
8183
Type Parameters:
8284
InputSchema: Schema for the user input, must be a subclass of BaseIOSchema.
@@ -92,6 +94,39 @@ class AtomicAgent[InputSchema: BaseIOSchema, OutputSchema: BaseIOSchema]:
9294
current_user_input (Optional[InputSchema]): The current user input being processed.
9395
model_api_parameters (dict): Additional parameters passed to the API provider.
9496
- Use this for parameters like 'temperature', 'max_tokens', etc.
97+
98+
Hook System:
99+
The AtomicAgent integrates with Instructor's hook system to provide comprehensive monitoring
100+
and error handling capabilities. Supported events include:
101+
102+
- 'parse:error': Triggered when Pydantic validation fails
103+
- 'completion:kwargs': Triggered before completion request
104+
- 'completion:response': Triggered after completion response
105+
- 'completion:error': Triggered on completion errors
106+
- 'completion:last_attempt': Triggered on final retry attempt
107+
108+
Hook Methods:
109+
- register_hook(event, handler): Register a hook handler for an event
110+
- unregister_hook(event, handler): Remove a hook handler
111+
- clear_hooks(event=None): Clear hooks for specific event or all events
112+
- enable_hooks()/disable_hooks(): Control hook processing
113+
- hooks_enabled: Property to check if hooks are enabled
114+
115+
Example:
116+
```python
117+
# Basic usage
118+
agent = AtomicAgent[InputSchema, OutputSchema](config)
119+
120+
# Register parse error hook for intelligent retry handling
121+
def handle_parse_error(error):
122+
print(f"Validation failed: {error}")
123+
# Implement custom retry logic, logging, etc.
124+
125+
agent.register_hook("parse:error", handle_parse_error)
126+
127+
# Now parse:error hooks will fire on validation failures
128+
response = agent.run(user_input)
129+
```
95130
"""
96131

97132
def __init__(self, config: AgentConfig):
@@ -110,6 +145,10 @@ def __init__(self, config: AgentConfig):
110145
self.current_user_input = None
111146
self.model_api_parameters = config.model_api_parameters or {}
112147

148+
# Hook management attributes
149+
self._hook_handlers: Dict[str, List[Callable]] = {}
150+
self._hooks_enabled: bool = True
151+
113152
def reset_history(self):
114153
"""
115154
Resets the history to its initial state.
@@ -318,6 +357,90 @@ def unregister_context_provider(self, provider_name: str):
318357
else:
319358
raise KeyError(f"Context provider '{provider_name}' not found.")
320359

360+
# Hook Management Methods
361+
def register_hook(self, event: str, handler: Callable) -> None:
362+
"""
363+
Registers a hook handler for a specific event.
364+
365+
Args:
366+
event (str): The event name (e.g., 'parse:error', 'completion:kwargs', etc.)
367+
handler (Callable): The callback function to handle the event
368+
"""
369+
if event not in self._hook_handlers:
370+
self._hook_handlers[event] = []
371+
self._hook_handlers[event].append(handler)
372+
373+
# Register with instructor client if it supports hooks
374+
if hasattr(self.client, "on"):
375+
self.client.on(event, handler)
376+
377+
def unregister_hook(self, event: str, handler: Callable) -> None:
378+
"""
379+
Unregisters a hook handler for a specific event.
380+
381+
Args:
382+
event (str): The event name
383+
handler (Callable): The callback function to remove
384+
"""
385+
if event in self._hook_handlers and handler in self._hook_handlers[event]:
386+
self._hook_handlers[event].remove(handler)
387+
388+
# Remove from instructor client if it supports hooks
389+
if hasattr(self.client, "off"):
390+
self.client.off(event, handler)
391+
392+
def clear_hooks(self, event: Optional[str] = None) -> None:
393+
"""
394+
Clears hook handlers for a specific event or all events.
395+
396+
Args:
397+
event (Optional[str]): The event name to clear, or None to clear all
398+
"""
399+
if event:
400+
if event in self._hook_handlers:
401+
# Clear from instructor client first
402+
if hasattr(self.client, "clear"):
403+
self.client.clear(event)
404+
self._hook_handlers[event].clear()
405+
else:
406+
# Clear all hooks
407+
if hasattr(self.client, "clear"):
408+
self.client.clear()
409+
self._hook_handlers.clear()
410+
411+
def _dispatch_hook(self, event: str, *args, **kwargs) -> None:
412+
"""
413+
Internal method to dispatch hook events with error isolation.
414+
415+
Args:
416+
event (str): The event name
417+
*args: Arguments to pass to handlers
418+
**kwargs: Keyword arguments to pass to handlers
419+
"""
420+
if not self._hooks_enabled or event not in self._hook_handlers:
421+
return
422+
423+
for handler in self._hook_handlers[event]:
424+
try:
425+
handler(*args, **kwargs)
426+
except Exception as e:
427+
# Log error but don't interrupt main flow
428+
logger = logging.getLogger(__name__)
429+
logger.warning(f"Hook handler for '{event}' raised exception: {e}")
430+
431+
def enable_hooks(self) -> None:
432+
"""Enable hook processing."""
433+
self._hooks_enabled = True
434+
435+
def disable_hooks(self) -> None:
436+
"""Disable hook processing."""
437+
self._hooks_enabled = False
438+
439+
@property
440+
def hooks_enabled(self) -> bool:
441+
"""Check if hooks are enabled."""
442+
return self._hooks_enabled
443+
321444

322445
if __name__ == "__main__":
323446
from rich.console import Console

0 commit comments

Comments
 (0)