diff --git a/.gitignore b/.gitignore index b7faf40..453b6d3 100644 --- a/.gitignore +++ b/.gitignore @@ -205,3 +205,5 @@ cython_debug/ marimo/_static/ marimo/_lsp/ __marimo__/ +CHANGELOG.md +MIGRATION_SUMMARY.md diff --git a/CHANGELOG.md b/CHANGELOG.md index c685176..5d9d991 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,17 +7,55 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.2.0] - 2026-02-10 + ### Added -- Initial SDK structure -- Core monitoring capabilities -- Framework integrations (sklearn, PyTorch, TensorFlow, Transformers, LangChain, XGBoost, LightGBM) -- Offline mode with persistent queue -- Privacy filters and PII detection -- Local caching with TTL support -- Decorator-based monitoring -- Async/sync interfaces -- Drift detection -- Comprehensive documentation +- **Git Integration**: Automatic Git context detection for model versioning + - `GitContext` class for repository metadata + - `detect_git_context()` function for auto-detection + - `validate_git_context()` for validation + - Support for both GitPython and subprocess fallback +- **CrewAI Multi-Agent Monitoring**: Full support for CrewAI workflows + - `CrewAIMonitor` class for monitoring crews + - Automatic agent and task tracking + - Agent-to-agent interaction logging + - Token usage and cost tracking + - Workflow analytics +- **LangChain Multi-Agent Support**: Enhanced LangChain integration + - `MultiAgentCallbackHandler` for agent execution tracking + - `LangGraphMultiAgentMonitor` for LangGraph workflows + - Agent-to-agent handoff monitoring + - Tool call tracking + - `monitor_langchain_agent()` helper function +- **Documentation**: Complete MkDocs setup with Material theme + - Comprehensive navigation structure + - API reference integration + - Code highlighting and copy buttons + - Dark/light mode support + +### Changed +- **BREAKING**: Fixed import paths from `explainai.*` to `whiteboxai.*` + - Users must update imports: `from explainai.client` → `from whiteboxai.client` +- Updated dependencies: + - httpx: >=0.24.0 → >=0.25.0 + - numpy: >=1.24.0 (aligned with latest stable) + - Added pandas>=1.3.0 (core dependency) + - Added tenacity>=8.0.0 (core dependency) +- Enhanced optional dependencies: + - Added git extra: `pip install whiteboxai-sdk[git]` + - Added crewai extra: `pip install whiteboxai-sdk[crewai]` + - Updated all extra: includes git, crewai, and all integrations + +### Fixed +- Import errors due to incorrect package naming (explainai vs whiteboxai) +- Missing MkDocs configuration causing documentation build failures +- Incomplete integration exports in `whiteboxai.integrations` + +### Documentation +- Created comprehensive MkDocs configuration +- Added index page with quick start examples +- Organized documentation with clear navigation +- Added examples for Git integration, CrewAI, and LangChain agents ## [0.1.0] - 2026-01-05 @@ -40,5 +78,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - PII detection and masking - Secure API key handling -[Unreleased]: https://github.com/AgentaFlow/whitebox-python-sdk/compare/v0.1.0...HEAD +[Unreleased]: https://github.com/AgentaFlow/whitebox-python-sdk/compare/v0.2.0...HEAD +[0.2.0]: https://github.com/AgentaFlow/whitebox-python-sdk/compare/v0.1.0...v0.2.0 [0.1.0]: https://github.com/AgentaFlow/whitebox-python-sdk/releases/tag/v0.1.0 diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..1f43b70 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,202 @@ +# WhiteBoxAI Python SDK + +Official Python SDK for integrating WhiteBoxAI monitoring into your ML applications. + +## Features + +- 🚀 **Easy Integration** - Monitor models with just a few lines of code +- 📊 **Framework Support** - Native integrations for Scikit-learn, PyTorch, TensorFlow, XGBoost, and more +- 🎯 **Decorator-based Monitoring** - Zero-code-change monitoring with decorators +- ⚡ **Async/Sync Interfaces** - Support for both synchronous and asynchronous workflows +- 🔒 **Privacy-First** - Built-in PII detection and data masking +- 💾 **Local Caching** - TTL-based caching to reduce API calls +- 📈 **Drift Detection** - Automatic model and data drift monitoring +- 🎨 **Flexible Configuration** - Extensive configuration options and feature flags +- 🔍 **Git Integration** - Automatic Git context detection for model versioning +- 🤖 **Multi-Agent Support** - Monitor CrewAI and LangChain multi-agent workflows + +## Installation + +```bash +pip install whiteboxai-sdk + +# With specific framework support +pip install whiteboxai-sdk[sklearn] +pip install whiteboxai-sdk[pytorch] +pip install whiteboxai-sdk[langchain] +pip install whiteboxai-sdk[crewai] +pip install whiteboxai-sdk[all] # All integrations +``` + +## Quick Start + +### Basic Usage + +```python +from whiteboxai import WhiteBoxAI, ModelMonitor + +# Initialize client +client = WhiteBoxAI(api_key="your-api-key") + +# Create monitor +monitor = ModelMonitor(client) + +# Register model +model_id = monitor.register_model( + name="fraud_detection", + model_type="classification", + framework="sklearn" +) + +# Log predictions +monitor.log_prediction( + inputs={"amount": 100.0, "merchant": "store_123"}, + output={"fraud_probability": 0.15, "prediction": "legitimate"} +) +``` + +### Git Integration + +```python +from whiteboxai import WhiteBoxAI, detect_git_context + +# Auto-detect Git context +git_context = detect_git_context() + +# Initialize with Git context +client = WhiteBoxAI(api_key="your-api-key") +model_id = client.models.register( + name="my_model", + **git_context.to_dict() # Include Git metadata +) +``` + +### CrewAI Multi-Agent Monitoring + +```python +from whiteboxai.integrations import CrewAIMonitor +from crewai import Agent, Task, Crew + +# Initialize monitor +monitor = CrewAIMonitor(api_key="your-api-key") + +# Define your crew +crew = Crew(agents=[...], tasks=[...]) + +# Start monitoring +workflow_id = monitor.start_monitoring( + crew=crew, + workflow_name="Research Workflow" +) + +# Execute crew +result = crew.kickoff() + +# Complete monitoring +summary = monitor.complete_monitoring(outputs={"result": result}) +``` + +### LangChain Multi-Agent Monitoring + +```python +from whiteboxai.integrations import LangGraphMultiAgentMonitor + +# Create monitor +monitor = LangGraphMultiAgentMonitor( + client=client, + workflow_name="Multi-Agent Research" +) + +# Start monitoring +workflow_id = monitor.start_monitoring() + +# Register agents +monitor.register_agent("supervisor", role="Coordinates agents") +monitor.register_agent("researcher", role="Gathers information") + +# Execute with callbacks +result = agent_executor.run( + callbacks=monitor.get_callbacks("researcher") +) + +# Complete monitoring +summary = monitor.complete_monitoring(outputs={"result": result}) +``` + +## Framework Integrations + +### Scikit-learn + +```python +from whiteboxai.integrations import SklearnMonitor +from sklearn.ensemble import RandomForestClassifier + +# Wrap your model +monitor = SklearnMonitor(client=client, model_id=model_id) +model = RandomForestClassifier() +wrapped_model = monitor.wrap(model) + +# Use as normal - monitoring happens automatically +wrapped_model.fit(X_train, y_train) +predictions = wrapped_model.predict(X_test) +``` + +### PyTorch + +```python +from whiteboxai.integrations import TorchMonitor +import torch.nn as nn + +# Monitor your model +monitor = TorchMonitor(client=client, model_id=model_id) +model = MyNeuralNetwork() +monitor.attach(model) + +# Training is automatically monitored +for epoch in range(num_epochs): + train(model, train_loader) +``` + +### TensorFlow/Keras + +```python +from whiteboxai.integrations import KerasMonitor + +# Add callback +monitor = KerasMonitor(client=client, model_id=model_id) +model.fit( + X_train, y_train, + callbacks=[monitor.get_callback()], + epochs=10 +) +``` + +### LangChain + +```python +from whiteboxai.integrations import LangChainMonitor + +# Monitor chain execution +monitor = LangChainMonitor(client=client) +callback = monitor.create_callback() + +chain.run("question", callbacks=[callback]) +``` + +## Documentation + +- [Getting Started Guide](getting-started.md) - Detailed installation and setup +- [Integration Guides](integrations.md) - Framework-specific integration tutorials +- [Offline Mode](offline-mode.md) - Running without internet connectivity +- [Production Deployment](PRODUCTION_DEPLOYMENT.md) - Best practices for production +- [API Reference](api-reference.md) - Complete API documentation + +## Support + +- **Documentation**: [Full Documentation](https://github.com/AgentaFlow/whitebox-python-sdk) +- **Issues**: [GitHub Issues](https://github.com/AgentaFlow/whitebox-python-sdk/issues) +- **Community**: [Discussions](https://github.com/AgentaFlow/whitebox-python-sdk/discussions) + +## License + +MIT License - see [LICENSE](https://github.com/AgentaFlow/whitebox-python-sdk/blob/main/LICENSE) for details. diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..016139d --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,92 @@ +site_name: WhiteBoxAI Python SDK +site_description: Official Python SDK for WhiteBoxAI - AI Observability & Explainability Platform +site_author: AgentaFlow +site_url: https://github.com/AgentaFlow/whitebox-python-sdk + +repo_name: AgentaFlow/whitebox-python-sdk +repo_url: https://github.com/AgentaFlow/whitebox-python-sdk +edit_uri: edit/main/docs/ + +theme: + name: material + palette: + # Palette toggle for light mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: indigo + accent: indigo + toggle: + icon: material/brightness-7 + name: Switch to dark mode + # Palette toggle for dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: indigo + accent: indigo + toggle: + icon: material/brightness-4 + name: Switch to light mode + features: + - navigation.tabs + - navigation.sections + - navigation.expand + - navigation.top + - search.suggest + - search.highlight + - content.code.copy + - content.code.annotate + +plugins: + - search + - mkdocstrings: + handlers: + python: + paths: [src] + options: + docstring_style: google + show_source: true + +markdown_extensions: + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences + - pymdownx.tabbed: + alternate_style: true + - admonition + - pymdownx.details + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - attr_list + - md_in_html + - toc: + permalink: true + +nav: + - Home: index.md + - Getting Started: + - Installation: getting-started.md + - Offline Mode: offline-mode.md + - Integrations: + - Overview: integrations.md + - Scikit-learn: SKLEARN_INTEGRATION.md + - PyTorch: PYTORCH_INTEGRATION.md + - TensorFlow: TENSORFLOW_INTEGRATION.md + - Hugging Face: HUGGINGFACE_INTEGRATION.md + - LangChain: LANGCHAIN_INTEGRATION.md + - Deployment: + - Production: PRODUCTION_DEPLOYMENT.md + - API Reference: api-reference.md + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/AgentaFlow/whitebox-python-sdk + - icon: fontawesome/brands/python + link: https://pypi.org/project/whiteboxai-sdk/ + +copyright: Copyright © 2026 AgentaFlow diff --git a/pyproject.toml b/pyproject.toml index 4d3235d..9822b92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "whiteboxai-sdk" -version = "0.1.0" +version = "0.2.0" description = "Official Python SDK for WhiteBoxAI - ML Monitoring and Observability" readme = "README.md" authors = [ @@ -26,10 +26,12 @@ classifiers = [ keywords = ["machine-learning", "explainability", "monitoring", "observability", "xai", "mlops", "whiteboxai"] requires-python = ">=3.9" dependencies = [ - "httpx>=0.24.0", + "httpx>=0.25.0", "pydantic>=2.0.0", "python-dotenv>=1.0.0", "numpy>=1.24.0", + "pandas>=1.3.0", + "tenacity>=8.0.0", ] [project.optional-dependencies] @@ -45,18 +47,22 @@ dev = [ "mypy>=1.7.0", "bandit>=1.7.5", ] +git = ["gitpython>=3.1.0"] sklearn = ["scikit-learn>=1.3.0"] pytorch = ["torch>=2.0.0"] tensorflow = ["tensorflow>=2.13.0"] transformers = ["transformers>=4.30.0"] langchain = ["langchain>=0.0.200"] +crewai = ["crewai>=0.1.0"] boosting = ["xgboost>=1.7.0", "lightgbm>=4.0.0"] all = [ + "gitpython>=3.1.0", "scikit-learn>=1.3.0", "torch>=2.0.0", "tensorflow>=2.13.0", "transformers>=4.30.0", "langchain>=0.0.200", + "crewai>=0.1.0", "xgboost>=1.7.0", "lightgbm>=4.0.0", ] diff --git a/src/whiteboxai/__init__.py b/src/whiteboxai/__init__.py index 5dd6441..922f81c 100644 --- a/src/whiteboxai/__init__.py +++ b/src/whiteboxai/__init__.py @@ -4,17 +4,21 @@ Official Python SDK for WhiteBoxAI - AI Observability & Explainability Platform. """ -__version__ = "0.1.0" +__version__ = "0.2.0" __author__ = "WhiteBoxAI Team" __license__ = "MIT" -from explainai.client import WhiteBoxAI -from explainai.decorators import monitor_model, monitor_prediction -from explainai.monitor import ModelMonitor +from whiteboxai.client import WhiteBoxAI +from whiteboxai.decorators import monitor_model, monitor_prediction +from whiteboxai.monitor import ModelMonitor +from whiteboxai.git_utils import GitContext, detect_git_context, validate_git_context __all__ = [ "WhiteBoxAI", "ModelMonitor", "monitor_model", "monitor_prediction", + "GitContext", + "detect_git_context", + "validate_git_context", ] diff --git a/src/whiteboxai/__version__.py b/src/whiteboxai/__version__.py index 7ffdace..afa047f 100644 --- a/src/whiteboxai/__version__.py +++ b/src/whiteboxai/__version__.py @@ -1,6 +1,6 @@ """Version information for WhiteBoxAI SDK.""" -__version__ = "0.1.0" +__version__ = "0.2.0" __author__ = "AgentaFlow" __email__ = "support@agentaflow.com" __license__ = "MIT" diff --git a/src/whiteboxai/git_utils.py b/src/whiteboxai/git_utils.py new file mode 100644 index 0000000..6d21bc9 --- /dev/null +++ b/src/whiteboxai/git_utils.py @@ -0,0 +1,338 @@ +""" +Git Auto-Detection Utilities + +Utilities for automatically detecting Git context (repository, commit, branch) +for model registration. +""" + +import logging +import os +from pathlib import Path +from typing import Dict, Optional +import subprocess + +logger = logging.getLogger(__name__) + + +class GitContext: + """Git repository context information.""" + + def __init__( + self, + repository_url: Optional[str] = None, + commit_sha: Optional[str] = None, + commit_message: Optional[str] = None, + commit_author: Optional[str] = None, + branch: Optional[str] = None, + tag: Optional[str] = None, + is_dirty: bool = False, + ): + """Initialize Git context.""" + self.repository_url = repository_url + self.commit_sha = commit_sha + self.commit_message = commit_message + self.commit_author = commit_author + self.branch = branch + self.tag = tag + self.is_dirty = is_dirty + + def to_dict(self) -> Dict[str, Optional[str]]: + """Convert to dictionary for API submission.""" + return { + "github_repository_url": self.repository_url, + "github_commit_hash": self.commit_sha, + "github_commit_message": self.commit_message, + "github_commit_author": self.commit_author, + "github_branch": self.branch, + "github_tag": self.tag, + } + + def __repr__(self) -> str: + """String representation.""" + parts = [] + if self.repository_url: + parts.append(f"repo={self.repository_url}") + if self.commit_sha: + parts.append(f"commit={self.commit_sha[:7]}") + if self.branch: + parts.append(f"branch={self.branch}") + if self.tag: + parts.append(f"tag={self.tag}") + return f"GitContext({', '.join(parts)})" + + +def detect_git_context(path: Optional[str] = None) -> Optional[GitContext]: + """ + Auto-detect Git context from the current directory or specified path. + + Args: + path: Path to check for Git repository (defaults to current directory) + + Returns: + GitContext object if Git repository found, None otherwise + + Example: + >>> context = detect_git_context() + >>> if context: + ... print(f"Repository: {context.repository_url}") + ... print(f"Commit: {context.commit_sha}") + ... print(f"Branch: {context.branch}") + """ + try: + import git + except ImportError: + logger.warning( + "GitPython not installed. Install with: pip install gitpython" + ) + return _detect_git_context_subprocess(path) + + try: + # Find repository + search_path = path or os.getcwd() + repo = git.Repo(search_path, search_parent_directories=True) + + # Get repository URL + repository_url = None + try: + remote = repo.remote("origin") + repository_url = remote.url + # Convert SSH URLs to HTTPS + if repository_url.startswith("git@github.com:"): + repository_url = repository_url.replace( + "git@github.com:", "https://github.com/" + ) + if repository_url.endswith(".git"): + repository_url = repository_url[:-4] + except Exception as e: + logger.debug(f"Could not get remote URL: {e}") + + # Get current commit + commit_sha = None + commit_message = None + commit_author = None + + try: + head_commit = repo.head.commit + commit_sha = head_commit.hexsha + commit_message = head_commit.message.strip() + commit_author = str(head_commit.author) + except Exception as e: + logger.debug(f"Could not get commit info: {e}") + + # Get current branch + branch = None + try: + if not repo.head.is_detached: + branch = repo.active_branch.name + except Exception as e: + logger.debug(f"Could not get branch: {e}") + + # Get current tag (if on a tag) + tag = None + try: + tags = [tag for tag in repo.tags if tag.commit == repo.head.commit] + if tags: + tag = tags[0].name + except Exception as e: + logger.debug(f"Could not get tag: {e}") + + # Check if working directory is dirty + is_dirty = repo.is_dirty(untracked_files=True) + if is_dirty: + logger.warning("Working directory has uncommitted changes") + + context = GitContext( + repository_url=repository_url, + commit_sha=commit_sha, + commit_message=commit_message, + commit_author=commit_author, + branch=branch, + tag=tag, + is_dirty=is_dirty, + ) + + logger.info(f"Detected Git context: {context}") + return context + + except git.InvalidGitRepositoryError: + logger.debug(f"No Git repository found at {search_path}") + return None + except Exception as e: + logger.error(f"Error detecting Git context: {e}") + return None + + +def _detect_git_context_subprocess(path: Optional[str] = None) -> Optional[GitContext]: + """ + Fallback Git detection using subprocess (when GitPython not available). + + Args: + path: Path to check for Git repository + + Returns: + GitContext object or None + """ + try: + cwd = path or os.getcwd() + + # Check if Git is available + subprocess.run( + ["git", "--version"], + capture_output=True, + check=True, + cwd=cwd, + ) + + # Get repository URL + repository_url = None + try: + result = subprocess.run( + ["git", "remote", "get-url", "origin"], + capture_output=True, + text=True, + check=True, + cwd=cwd, + ) + repository_url = result.stdout.strip() + + # Convert SSH to HTTPS + if repository_url.startswith("git@github.com:"): + repository_url = repository_url.replace( + "git@github.com:", "https://github.com/" + ) + if repository_url.endswith(".git"): + repository_url = repository_url[:-4] + except subprocess.CalledProcessError: + pass + + # Get commit SHA + commit_sha = None + try: + result = subprocess.run( + ["git", "rev-parse", "HEAD"], + capture_output=True, + text=True, + check=True, + cwd=cwd, + ) + commit_sha = result.stdout.strip() + except subprocess.CalledProcessError: + pass + + # Get commit message + commit_message = None + try: + result = subprocess.run( + ["git", "log", "-1", "--pretty=%B"], + capture_output=True, + text=True, + check=True, + cwd=cwd, + ) + commit_message = result.stdout.strip() + except subprocess.CalledProcessError: + pass + + # Get commit author + commit_author = None + try: + result = subprocess.run( + ["git", "log", "-1", "--pretty=%an <%ae>"], + capture_output=True, + text=True, + check=True, + cwd=cwd, + ) + commit_author = result.stdout.strip() + except subprocess.CalledProcessError: + pass + + # Get branch + branch = None + try: + result = subprocess.run( + ["git", "rev-parse", "--abbrev-ref", "HEAD"], + capture_output=True, + text=True, + check=True, + cwd=cwd, + ) + branch_output = result.stdout.strip() + if branch_output != "HEAD": # Not in detached HEAD + branch = branch_output + except subprocess.CalledProcessError: + pass + + # Get tag + tag = None + try: + result = subprocess.run( + ["git", "describe", "--exact-match", "--tags", "HEAD"], + capture_output=True, + text=True, + check=True, + cwd=cwd, + ) + tag = result.stdout.strip() + except subprocess.CalledProcessError: + pass + + # Check if dirty + is_dirty = False + try: + result = subprocess.run( + ["git", "status", "--porcelain"], + capture_output=True, + text=True, + check=True, + cwd=cwd, + ) + is_dirty = bool(result.stdout.strip()) + except subprocess.CalledProcessError: + pass + + context = GitContext( + repository_url=repository_url, + commit_sha=commit_sha, + commit_message=commit_message, + commit_author=commit_author, + branch=branch, + tag=tag, + is_dirty=is_dirty, + ) + + logger.info(f"Detected Git context (subprocess): {context}") + return context + + except (subprocess.CalledProcessError, FileNotFoundError): + logger.debug("Git not available or not a Git repository") + return None + except Exception as e: + logger.error(f"Error detecting Git context with subprocess: {e}") + return None + + +def validate_git_context(context: GitContext, require_clean: bool = False) -> bool: + """ + Validate Git context before model registration. + + Args: + context: GitContext object + require_clean: Whether to require a clean working directory + + Returns: + True if valid, False otherwise + """ + if not context: + logger.warning("No Git context provided") + return False + + if not context.commit_sha: + logger.error("Git context missing commit SHA") + return False + + if require_clean and context.is_dirty: + logger.error("Working directory has uncommitted changes (require_clean=True)") + return False + + return True diff --git a/src/whiteboxai/integrations/__init__.py b/src/whiteboxai/integrations/__init__.py index 8a2bb25..1260c4a 100644 --- a/src/whiteboxai/integrations/__init__.py +++ b/src/whiteboxai/integrations/__init__.py @@ -57,6 +57,38 @@ except ImportError: pass +# CrewAI integration +try: + from .crewai_monitor import CrewAIMonitor, monitor_crew + if '__all__' in dir(): + __all__.extend(['CrewAIMonitor', 'monitor_crew']) + else: + __all__ = ['CrewAIMonitor', 'monitor_crew'] +except ImportError: + pass + +# LangChain Multi-Agent integration +try: + from .langchain_agents import ( + MultiAgentCallbackHandler, + LangGraphMultiAgentMonitor, + monitor_langchain_agent + ) + if '__all__' in dir(): + __all__.extend([ + 'MultiAgentCallbackHandler', + 'LangGraphMultiAgentMonitor', + 'monitor_langchain_agent' + ]) + else: + __all__ = [ + 'MultiAgentCallbackHandler', + 'LangGraphMultiAgentMonitor', + 'monitor_langchain_agent' + ] +except ImportError: + pass + # Ensure __all__ exists even if all imports fail if '__all__' not in dir(): __all__ = [] diff --git a/src/whiteboxai/integrations/crewai_monitor.py b/src/whiteboxai/integrations/crewai_monitor.py new file mode 100644 index 0000000..4abe63b --- /dev/null +++ b/src/whiteboxai/integrations/crewai_monitor.py @@ -0,0 +1,489 @@ +""" +CrewAI Integration for WhiteBoxAI + +Monitor CrewAI multi-agent workflows with automatic tracking of agents, +tasks, interactions, and costs. +""" + +import logging +import time +import uuid +from typing import Optional, Dict, Any, List +from datetime import datetime + +logger = logging.getLogger(__name__) + + +class CrewAIMonitor: + """ + Monitor CrewAI multi-agent workflows. + + Automatically tracks: + - Workflow lifecycle (start, completion) + - Agent definitions and executions + - Task assignments and completions + - Agent-to-agent interactions + - Token usage and costs + + Example: + >>> from whiteboxai.integrations import CrewAIMonitor + >>> from crewai import Agent, Task, Crew + >>> + >>> monitor = CrewAIMonitor(api_key="your_api_key") + >>> + >>> # Define agents + >>> researcher = Agent( + ... role="Research Analyst", + ... goal="Find accurate information", + ... backstory="Expert researcher", + ... tools=[search_tool] + ... ) + >>> + >>> writer = Agent( + ... role="Content Writer", + ... goal="Write engaging content", + ... backstory="Professional writer", + ... tools=[writing_tool] + ... ) + >>> + >>> # Create tasks + >>> research_task = Task( + ... description="Research topic X", + ... agent=researcher + ... ) + >>> + >>> writing_task = Task( + ... description="Write article based on research", + ... agent=writer + ... ) + >>> + >>> # Create and monitor crew + >>> crew = Crew( + ... agents=[researcher, writer], + ... tasks=[research_task, writing_task], + ... process=Process.sequential + ... ) + >>> + >>> workflow_id = monitor.start_monitoring( + ... crew=crew, + ... workflow_name="Article Generation", + ... metadata={"topic": "AI Safety"} + ... ) + >>> + >>> # Execute crew (automatically monitored) + >>> result = crew.kickoff() + >>> + >>> # Complete monitoring + >>> monitor.complete_monitoring(outputs={"article": result}) + """ + + def __init__( + self, + api_key: str, + api_url: Optional[str] = None, + organization_id: Optional[str] = None + ): + """ + Initialize CrewAI monitor. + + Args: + api_key: WhiteBoxAI API key + api_url: WhiteBoxAI API URL (optional, defaults to production) + organization_id: Organization ID (optional, extracted from token if not provided) + """ + from whiteboxai import WhiteBoxAI + + self.client = WhiteBoxAI(api_key=api_key, base_url=api_url) if api_url else WhiteBoxAI(api_key=api_key) + self.organization_id = organization_id + self.workflow_id = None + self.agent_map = {} # Maps CrewAI agent to WhiteBoxAI agent_id + self.task_map = {} # Maps CrewAI task to WhiteBoxAI task_id + self.execution_map = {} # Maps agent to execution_id + + logger.info("CrewAI Monitor initialized") + + def start_monitoring( + self, + crew: Any, # crewai.Crew + workflow_name: str, + metadata: Optional[Dict[str, Any]] = None + ) -> str: + """ + Start monitoring a CrewAI crew workflow. + + Args: + crew: CrewAI Crew instance + workflow_name: Name for the workflow + metadata: Optional workflow metadata + + Returns: + Workflow ID (UUID string) + """ + try: + # Create workflow + workflow_data = { + "name": workflow_name, + "framework": "crewai", + "metadata": metadata or {} + } + + response = self.client.request( + "POST", + "/api/v1/workflows/multi-agent/start", + data=workflow_data + ) + + self.workflow_id = response.get("id") + logger.info(f"Started monitoring CrewAI workflow: {self.workflow_id}") + + # Register agents + for agent in crew.agents: + self._register_agent(agent) + + # Register tasks + for task in crew.tasks: + self._register_task(task) + + # Start workflow + start_data = { + "inputs": { + "agent_count": len(crew.agents), + "task_count": len(crew.tasks), + "process": str(crew.process) if hasattr(crew, "process") else "sequential" + } + } + + self.client.request( + "POST", + f"/api/v1/workflows/multi-agent/{self.workflow_id}/start", + data=start_data + ) + + return self.workflow_id + + except Exception as e: + logger.error(f"Error starting CrewAI monitoring: {str(e)}") + raise + + def _register_agent(self, crew_agent: Any) -> str: + """ + Register a CrewAI agent with WhiteBoxAI. + + Args: + crew_agent: CrewAI Agent instance + + Returns: + Agent ID (UUID string) + """ + try: + agent_data = { + "name": getattr(crew_agent, "role", "Unknown Agent"), + "role": getattr(crew_agent, "role", None), + "agent_type": "crewai_agent", + "goal": getattr(crew_agent, "goal", None), + "backstory": getattr(crew_agent, "backstory", None), + "tools": [tool.__class__.__name__ for tool in getattr(crew_agent, "tools", [])], + "llm_provider": getattr(getattr(crew_agent, "llm", None), "model_name", "unknown").split("/")[0] if hasattr(crew_agent, "llm") else None, + "model_name": getattr(getattr(crew_agent, "llm", None), "model_name", None) if hasattr(crew_agent, "llm") else None, + "metadata": { + "verbose": getattr(crew_agent, "verbose", False), + "allow_delegation": getattr(crew_agent, "allow_delegation", False), + "max_iter": getattr(crew_agent, "max_iter", None), + } + } + + response = self.client.request( + "POST", + f"/api/v1/workflows/multi-agent/{self.workflow_id}/agents", + data=agent_data + ) + + agent_id = response.get("id") + self.agent_map[id(crew_agent)] = agent_id + + logger.debug(f"Registered agent: {agent_data['name']} ({agent_id})") + + return agent_id + + except Exception as e: + logger.error(f"Error registering agent: {str(e)}") + raise + + def _register_task(self, crew_task: Any) -> str: + """ + Register a CrewAI task with WhiteBoxAI. + + Args: + crew_task: CrewAI Task instance + + Returns: + Task ID (UUID string) + """ + try: + # Get agent ID for task's assigned agent + agent_id = None + if hasattr(crew_task, "agent") and crew_task.agent: + agent_id = self.agent_map.get(id(crew_task.agent)) + + task_data = { + "task_name": getattr(crew_task, "description", "Unknown Task")[:255], + "description": getattr(crew_task, "description", None), + "expected_output": getattr(crew_task, "expected_output", None), + "agent_id": agent_id, + "context": { + "tools": [tool.__class__.__name__ for tool in getattr(crew_task, "tools", [])], + "async_execution": getattr(crew_task, "async_execution", False), + } + } + + response = self.client.request( + "POST", + f"/api/v1/workflows/multi-agent/{self.workflow_id}/tasks", + data=task_data + ) + + task_id = response.get("id") + self.task_map[id(crew_task)] = task_id + + logger.debug(f"Registered task: {task_data['task_name'][:50]}... ({task_id})") + + return task_id + + except Exception as e: + logger.error(f"Error registering task: {str(e)}") + raise + + def log_agent_execution( + self, + agent: Any, + inputs: Optional[Dict] = None, + outputs: Optional[Dict] = None, + tokens_used: int = 0, + cost: float = 0.0 + ) -> None: + """ + Log an agent execution. + + Args: + agent: CrewAI Agent instance + inputs: Execution inputs + outputs: Execution outputs + tokens_used: Tokens consumed + cost: Execution cost + """ + try: + agent_id = self.agent_map.get(id(agent)) + if not agent_id: + logger.warning(f"Agent not registered, skipping execution log") + return + + execution_data = { + "agent_id": agent_id, + "inputs": inputs, + "outputs": outputs, + "tokens_used": tokens_used, + "cost": cost, + "status": "completed" + } + + self.client.request( + "POST", + f"/api/v1/workflows/multi-agent/{self.workflow_id}/executions", + data=execution_data + ) + + logger.debug(f"Logged execution for agent: {agent_id}") + + except Exception as e: + logger.error(f"Error logging agent execution: {str(e)}") + + def log_task_completion( + self, + task: Any, + status: str = "completed", + output_data: Optional[Dict] = None, + error_message: Optional[str] = None + ) -> None: + """ + Log task completion. + + Args: + task: CrewAI Task instance + status: Task status (completed, failed) + output_data: Task output + error_message: Error message if failed + """ + try: + task_id = self.task_map.get(id(task)) + if not task_id: + logger.warning(f"Task not registered, skipping completion log") + return + + update_data = { + "status": status, + "output_data": output_data, + "error_message": error_message + } + + self.client.request( + "PATCH", + f"/api/v1/workflows/multi-agent/tasks/{task_id}", + data=update_data + ) + + logger.debug(f"Logged task completion: {task_id} ({status})") + + except Exception as e: + logger.error(f"Error logging task completion: {str(e)}") + + def log_interaction( + self, + from_agent: Any, + to_agent: Any, + interaction_type: str = "delegation", + message: Optional[str] = None + ) -> None: + """ + Log agent-to-agent interaction. + + Args: + from_agent: Source agent + to_agent: Target agent + interaction_type: Type of interaction (delegation, handoff, query, feedback) + message: Interaction message + """ + try: + from_agent_id = self.agent_map.get(id(from_agent)) + to_agent_id = self.agent_map.get(id(to_agent)) + + if not from_agent_id or not to_agent_id: + logger.warning("Agents not registered, skipping interaction log") + return + + interaction_data = { + "interaction_type": interaction_type, + "from_agent_id": from_agent_id, + "to_agent_id": to_agent_id, + "message": message + } + + self.client.request( + "POST", + f"/api/v1/workflows/multi-agent/{self.workflow_id}/interactions", + data=interaction_data + ) + + logger.debug(f"Logged interaction: {from_agent_id} -> {to_agent_id} ({interaction_type})") + + except Exception as e: + logger.error(f"Error logging interaction: {str(e)}") + + def complete_monitoring( + self, + status: str = "completed", + outputs: Optional[Dict] = None, + error_message: Optional[str] = None + ) -> Dict[str, Any]: + """ + Complete workflow monitoring. + + Args: + status: Final workflow status (completed, failed, cancelled) + outputs: Workflow outputs + error_message: Error message if failed + + Returns: + Workflow summary with analytics + """ + try: + complete_data = { + "status": status, + "outputs": outputs, + "error_message": error_message + } + + response = self.client.request( + "POST", + f"/api/v1/workflows/multi-agent/{self.workflow_id}/complete", + data=complete_data + ) + + logger.info(f"Completed monitoring workflow: {self.workflow_id} ({status})") + + # Get analytics + analytics = self.get_analytics() + + return { + "workflow_id": self.workflow_id, + "status": status, + "analytics": analytics + } + + except Exception as e: + logger.error(f"Error completing monitoring: {str(e)}") + raise + + def get_analytics(self) -> Dict[str, Any]: + """ + Get workflow analytics. + + Returns: + Dict with metrics, cost breakdown, and bottlenecks + """ + try: + if not self.workflow_id: + raise ValueError("No active workflow") + + analytics = self.client.request( + "GET", + f"/api/v1/workflows/multi-agent/{self.workflow_id}/analytics" + ) + + cost_breakdown = self.client.request( + "GET", + f"/api/v1/workflows/multi-agent/{self.workflow_id}/cost-breakdown" + ) + + return { + "metrics": analytics, + "cost_breakdown": cost_breakdown + } + + except Exception as e: + logger.error(f"Error getting analytics: {str(e)}") + return {} + + +# Helper function for easy usage +def monitor_crew( + crew: Any, + workflow_name: str, + api_key: str, + api_url: Optional[str] = None, + metadata: Optional[Dict] = None +) -> CrewAIMonitor: + """ + Convenience function to start monitoring a CrewAI crew. + + Args: + crew: CrewAI Crew instance + workflow_name: Workflow name + api_key: WhiteBoxAI API key + api_url: WhiteBoxAI API URL (optional) + metadata: Workflow metadata (optional) + + Returns: + CrewAIMonitor instance with workflow started + + Example: + >>> monitor = monitor_crew( + ... crew=my_crew, + ... workflow_name="Research & Writing", + ... api_key="your_api_key" + ... ) + >>> result = my_crew.kickoff() + >>> monitor.complete_monitoring(outputs={"result": result}) + """ + monitor = CrewAIMonitor(api_key=api_key, api_url=api_url) + monitor.start_monitoring(crew=crew, workflow_name=workflow_name, metadata=metadata) + return monitor diff --git a/src/whiteboxai/integrations/langchain_agents.py b/src/whiteboxai/integrations/langchain_agents.py new file mode 100644 index 0000000..3a746c4 --- /dev/null +++ b/src/whiteboxai/integrations/langchain_agents.py @@ -0,0 +1,609 @@ +""" +LangChain Multi-Agent Integration for WhiteBoxAI + +Enhanced callback handler for monitoring multi-agent LangChain workflows including: +- LangGraph multi-agent patterns +- Agent supervisors and coordinators +- Tool usage and agent handoffs +- Agent-to-agent communication +""" + +from typing import Any, Dict, List, Optional, Union +from datetime import datetime +from langchain.callbacks.base import BaseCallbackHandler +from langchain.schema import AgentAction, AgentFinish, LLMResult +from langchain.schema.document import Document +from langchain.schema.output import ChatGeneration, Generation + +try: + from whiteboxai import WhiteBoxAI +except ImportError: + WhiteBoxAI = None + + +class MultiAgentCallbackHandler(BaseCallbackHandler): + """Enhanced callback handler for multi-agent LangChain workflows. + + This handler tracks: + - Agent executions and decisions + - Tool calls and results + - Agent-to-agent handoffs + - LLM calls per agent + - Workflow-level metrics + + Example: + ```python + from langchain.agents import AgentExecutor, create_react_agent + from whiteboxai.integrations import MultiAgentCallbackHandler + + # Initialize WhiteBoxAI client + client = WhiteBoxAI(api_key="your_key") + + # Create workflow + workflow_id = client.agent_workflows.create( + name="Research Workflow", + framework="langchain" + ).id + + # Start workflow + client.agent_workflows.start(workflow_id) + + # Create callback + callback = MultiAgentCallbackHandler( + client=client, + workflow_id=workflow_id, + agent_name="researcher" + ) + + # Use with agent + agent_executor = AgentExecutor( + agent=agent, + tools=tools, + callbacks=[callback] + ) + result = agent_executor.run("Research AI safety") + + # Complete workflow + client.agent_workflows.complete( + workflow_id, + outputs={"result": result} + ) + ``` + """ + + def __init__( + self, + client: "WhiteBoxAI", + workflow_id: str, + agent_name: str = "main", + agent_role: Optional[str] = None, + track_tokens: bool = True, + track_costs: bool = True, + ): + """Initialize the callback handler. + + Args: + client: WhiteBoxAI client instance + workflow_id: ID of the workflow to track + agent_name: Name of the current agent + agent_role: Role/description of the agent + track_tokens: Whether to track token usage + track_costs: Whether to estimate costs + """ + if WhiteBoxAI is None: + raise ImportError( + "whiteboxai package not installed. " + "Install with: pip install whiteboxai" + ) + + self.client = client + self.workflow_id = workflow_id + self.agent_name = agent_name + self.agent_role = agent_role or agent_name + self.track_tokens = track_tokens + self.track_costs = track_costs + + # Tracking state + self.current_execution_id: Optional[str] = None + self.execution_start_time: Optional[datetime] = None + self.llm_call_count = 0 + self.tool_call_count = 0 + self.total_tokens = 0 + self.total_cost = 0.0 + self.execution_inputs: Optional[Dict[str, Any]] = None + + def on_chain_start( + self, + serialized: Dict[str, Any], + inputs: Dict[str, Any], + **kwargs: Any + ) -> None: + """Run when chain starts.""" + # Start agent execution + self.execution_start_time = datetime.utcnow() + self.execution_inputs = inputs + self.llm_call_count = 0 + self.tool_call_count = 0 + self.total_tokens = 0 + self.total_cost = 0.0 + + def on_chain_end( + self, + outputs: Dict[str, Any], + **kwargs: Any + ) -> None: + """Run when chain ends successfully.""" + if self.execution_start_time: + duration_ms = int( + (datetime.utcnow() - self.execution_start_time).total_seconds() * 1000 + ) + + # Log agent execution + try: + response = self.client.agent_workflows.create_execution( + workflow_id=self.workflow_id, + agent_name=self.agent_name, + status="completed", + inputs=self.execution_inputs, + outputs=outputs, + duration_ms=duration_ms, + llm_call_count=self.llm_call_count, + tool_call_count=self.tool_call_count, + tokens_used=self.total_tokens if self.track_tokens else None, + cost=self.total_cost if self.track_costs else None, + ) + self.current_execution_id = response.get("id") + except Exception as e: + print(f"Warning: Failed to log execution: {e}") + + # Reset state + self.execution_start_time = None + + def on_chain_error( + self, + error: Union[Exception, KeyboardInterrupt], + **kwargs: Any + ) -> None: + """Run when chain errors.""" + if self.execution_start_time: + duration_ms = int( + (datetime.utcnow() - self.execution_start_time).total_seconds() * 1000 + ) + + # Log failed execution + try: + self.client.agent_workflows.create_execution( + workflow_id=self.workflow_id, + agent_name=self.agent_name, + status="failed", + inputs=self.execution_inputs, + outputs={"error": str(error)}, + duration_ms=duration_ms, + llm_call_count=self.llm_call_count, + tool_call_count=self.tool_call_count, + ) + except Exception as e: + print(f"Warning: Failed to log error: {e}") + + self.execution_start_time = None + + def on_llm_start( + self, + serialized: Dict[str, Any], + prompts: List[str], + **kwargs: Any + ) -> None: + """Run when LLM starts.""" + self.llm_call_count += 1 + + def on_llm_end( + self, + response: LLMResult, + **kwargs: Any + ) -> None: + """Run when LLM ends.""" + # Track tokens if available + if self.track_tokens and hasattr(response, "llm_output"): + llm_output = response.llm_output or {} + token_usage = llm_output.get("token_usage", {}) + + total = token_usage.get("total_tokens", 0) + self.total_tokens += total + + # Estimate cost if tracking + if self.track_costs and total > 0: + # Rough estimate: $0.002 per 1K tokens (GPT-3.5 pricing) + self.total_cost += (total / 1000) * 0.002 + + def on_agent_action( + self, + action: AgentAction, + **kwargs: Any + ) -> None: + """Run when agent takes an action (tool call).""" + self.tool_call_count += 1 + + # Log tool call as interaction + try: + self.client.agent_workflows.create_interaction( + workflow_id=self.workflow_id, + from_agent=self.agent_name, + to_agent="tool", + interaction_type="tool_call", + message=f"Tool: {action.tool}, Input: {action.tool_input}", + meta_data={ + "tool": action.tool, + "tool_input": action.tool_input, + "log": action.log, + } + ) + except Exception as e: + print(f"Warning: Failed to log tool call: {e}") + + def on_agent_finish( + self, + finish: AgentFinish, + **kwargs: Any + ) -> None: + """Run when agent finishes execution.""" + # This is called when the agent completes its reasoning + pass + + def on_tool_start( + self, + serialized: Dict[str, Any], + input_str: str, + **kwargs: Any + ) -> None: + """Run when tool starts.""" + pass + + def on_tool_end( + self, + output: str, + **kwargs: Any + ) -> None: + """Run when tool ends.""" + # Log tool result as interaction + try: + self.client.agent_workflows.create_interaction( + workflow_id=self.workflow_id, + from_agent="tool", + to_agent=self.agent_name, + interaction_type="response", + message=f"Tool result: {output[:500]}", # Truncate long outputs + meta_data={"output": output} + ) + except Exception as e: + print(f"Warning: Failed to log tool result: {e}") + + def on_tool_error( + self, + error: Union[Exception, KeyboardInterrupt], + **kwargs: Any + ) -> None: + """Run when tool errors.""" + try: + self.client.agent_workflows.create_interaction( + workflow_id=self.workflow_id, + from_agent="tool", + to_agent=self.agent_name, + interaction_type="response", + message=f"Tool error: {str(error)}", + meta_data={"error": str(error), "error_type": type(error).__name__} + ) + except Exception as e: + print(f"Warning: Failed to log tool error: {e}") + + def on_text( + self, + text: str, + **kwargs: Any + ) -> None: + """Run on arbitrary text.""" + pass + + +class LangGraphMultiAgentMonitor: + """Monitor for LangGraph multi-agent workflows. + + Provides higher-level monitoring for LangGraph patterns like: + - Agent supervisors + - Agent networks + - Sequential/parallel agent execution + + Example: + ```python + from langgraph.graph import StateGraph + from whiteboxai.integrations import LangGraphMultiAgentMonitor + + # Create monitor + monitor = LangGraphMultiAgentMonitor( + client=client, + workflow_name="Multi-Agent Research" + ) + + # Start monitoring + workflow_id = monitor.start_monitoring() + + # Register agents + monitor.register_agent("supervisor", role="Coordinates other agents") + monitor.register_agent("researcher", role="Gathers information") + monitor.register_agent("writer", role="Writes content") + + # Execute graph with callbacks + graph = StateGraph(...) + result = graph.invoke( + inputs, + config={"callbacks": [monitor.get_callbacks("supervisor")]} + ) + + # Complete monitoring + monitor.complete_monitoring(outputs={"result": result}) + ``` + """ + + def __init__( + self, + client: "WhiteBoxAI", + workflow_name: str, + meta_data: Optional[Dict[str, Any]] = None + ): + """Initialize the LangGraph monitor. + + Args: + client: WhiteBoxAI client instance + workflow_name: Name for the workflow + meta_data: Additional meta_data to attach + """ + if WhiteBoxAI is None: + raise ImportError( + "whiteboxai package not installed. " + "Install with: pip install whiteboxai" + ) + + self.client = client + self.workflow_name = workflow_name + self.workflow_meta_data = meta_data or {} + self.workflow_id: Optional[str] = None + self.callbacks: Dict[str, MultiAgentCallbackHandler] = {} + self.start_time: Optional[datetime] = None + + def start_monitoring(self, inputs: Optional[Dict[str, Any]] = None) -> str: + """Start workflow monitoring. + + Args: + inputs: Initial workflow inputs + + Returns: + workflow_id: ID of the created workflow + """ + self.start_time = datetime.utcnow() + + # Create workflow + response = self.client.agent_workflows.create( + name=self.workflow_name, + framework="langchain", + inputs=inputs, + meta_data=self.workflow_meta_data + ) + self.workflow_id = response.get("id") + + # Start workflow + self.client.agent_workflows.start(self.workflow_id) + + return self.workflow_id + + def register_agent( + self, + agent_name: str, + role: Optional[str] = None, + model_name: Optional[str] = None, + tools: Optional[List[str]] = None, + **kwargs + ) -> None: + """Register an agent in the workflow. + + Args: + agent_name: Name of the agent + role: Agent's role/goal + model_name: LLM model used + tools: List of tool names + **kwargs: Additional agent configuration + """ + if not self.workflow_id: + raise ValueError("Must call start_monitoring() first") + + self.client.agent_workflows.register_agent( + workflow_id=self.workflow_id, + name=agent_name, + role=role or agent_name, + model_name=model_name, + tools=tools, + **kwargs + ) + + def get_callbacks( + self, + agent_name: str, + agent_role: Optional[str] = None + ) -> List[BaseCallbackHandler]: + """Get callbacks for a specific agent. + + Args: + agent_name: Name of the agent + agent_role: Optional role description + + Returns: + List of callback handlers + """ + if not self.workflow_id: + raise ValueError("Must call start_monitoring() first") + + if agent_name not in self.callbacks: + self.callbacks[agent_name] = MultiAgentCallbackHandler( + client=self.client, + workflow_id=self.workflow_id, + agent_name=agent_name, + agent_role=agent_role + ) + + return [self.callbacks[agent_name]] + + def log_handoff( + self, + from_agent: str, + to_agent: str, + message: str, + meta_data: Optional[Dict[str, Any]] = None + ) -> None: + """Log an agent-to-agent handoff. + + Args: + from_agent: Agent passing control + to_agent: Agent receiving control + message: Handoff message/context + meta_data: Additional meta_data + """ + if not self.workflow_id: + raise ValueError("Must call start_monitoring() first") + + self.client.agent_workflows.create_interaction( + workflow_id=self.workflow_id, + from_agent=from_agent, + to_agent=to_agent, + interaction_type="handoff", + message=message, + meta_data=meta_data + ) + + def complete_monitoring( + self, + outputs: Optional[Dict[str, Any]] = None, + status: str = "completed" + ) -> Dict[str, Any]: + """Complete workflow monitoring. + + Args: + outputs: Final workflow outputs + status: Workflow status (completed/failed) + + Returns: + Summary with analytics + """ + if not self.workflow_id: + raise ValueError("Must call start_monitoring() first") + + # Complete workflow + self.client.agent_workflows.complete( + workflow_id=self.workflow_id, + outputs=outputs, + status=status + ) + + # Get analytics + try: + analytics = self.client.agent_workflows.get_analytics(self.workflow_id) + return { + "workflow_id": self.workflow_id, + "status": status, + "outputs": outputs, + "analytics": analytics + } + except Exception as e: + print(f"Warning: Failed to retrieve analytics: {e}") + return { + "workflow_id": self.workflow_id, + "status": status, + "outputs": outputs + } + + +def monitor_langchain_agent( + client: "WhiteBoxAI", + agent_executor: Any, + workflow_name: str, + agent_name: str = "main", + inputs: Optional[Dict[str, Any]] = None, + **run_kwargs +) -> Dict[str, Any]: + """Helper function to monitor a single LangChain agent execution. + + Args: + client: WhiteBoxAI client + agent_executor: LangChain AgentExecutor instance + workflow_name: Name for the workflow + agent_name: Name of the agent + inputs: Inputs to the agent + **run_kwargs: Additional arguments to pass to agent.run() + + Returns: + Dict with result and workflow_id + + Example: + ```python + from langchain.agents import AgentExecutor, create_react_agent + from whiteboxai.integrations import monitor_langchain_agent + + result_dict = monitor_langchain_agent( + client=client, + agent_executor=agent_executor, + workflow_name="Research Task", + agent_name="researcher", + inputs={"input": "Research AI safety"} + ) + + print(f"Result: {result_dict['result']}") + print(f"Workflow ID: {result_dict['workflow_id']}") + ``` + """ + # Create workflow + response = client.agent_workflows.create( + name=workflow_name, + framework="langchain", + inputs=inputs + ) + workflow_id = response.get("id") + + # Start workflow + client.agent_workflows.start(workflow_id) + + # Create callback + callback = MultiAgentCallbackHandler( + client=client, + workflow_id=workflow_id, + agent_name=agent_name + ) + + try: + # Run agent with callback + result = agent_executor.run( + callbacks=[callback], + **run_kwargs + ) + + # Complete workflow + client.agent_workflows.complete( + workflow_id, + outputs={"result": result} + ) + + return { + "result": result, + "workflow_id": workflow_id, + "status": "completed" + } + except Exception as e: + # Log failure + client.agent_workflows.complete( + workflow_id, + outputs={"error": str(e)}, + status="failed" + ) + + return { + "result": None, + "workflow_id": workflow_id, + "status": "failed", + "error": str(e) + }