diff --git a/cookbook/README.md b/cookbook/README.md index 9e5cf2ca..52909fee 100644 --- a/cookbook/README.md +++ b/cookbook/README.md @@ -2,6 +2,10 @@ This cookbook collects small, focused recipes showing how to accomplish common tasks with the GitHub Copilot SDK across languages. Each recipe is intentionally short and practical, with copy‑pasteable snippets and pointers to fuller examples and tests. +## Prerequisites + + Refer to the [Getting Started guide](../docs/getting-started.md#prerequisites) for installation instructions across all supported languages. + ## Recipes by Language ### .NET (C#) @@ -22,11 +26,16 @@ This cookbook collects small, focused recipes showing how to accomplish common t ### Python +- [Custom Agents](python/custom-agents.md): Create specialized agents with custom system prompts and behaviors. +- [Custom Providers](python/custom-providers.md): Implement custom model providers for specialized AI backends. +- [Custom Tools](python/custom-tools.md): Build custom tools to extend agent capabilities with domain-specific functions. - [Error Handling](python/error-handling.md): Handle errors gracefully including connection failures, timeouts, and cleanup. -- [Multiple Sessions](python/multiple-sessions.md): Manage multiple independent conversations simultaneously. - [Managing Local Files](python/managing-local-files.md): Organize files by metadata using AI-powered grouping strategies. -- [PR Visualization](python/pr-visualization.md): Generate interactive PR age charts using GitHub MCP Server. +- [MCP Servers](python/mcp-servers.md): Integrate Model Context Protocol servers for extended functionality. +- [Multiple Sessions](python/multiple-sessions.md): Manage multiple independent conversations simultaneously. - [Persisting Sessions](python/persisting-sessions.md): Save and resume sessions across restarts. +- [PR Visualization](python/pr-visualization.md): Generate interactive PR age charts using GitHub MCP Server. +- [Streaming Responses](python/streaming-responses.md): Stream AI responses in real-time for better user experience. ### Go @@ -80,7 +89,3 @@ go run .go - Propose or add a new recipe by creating a markdown file in your language's `cookbook/` folder and a runnable example in `recipe/` - Follow repository guidance in [CONTRIBUTING.md](../CONTRIBUTING.md) - -## Status - -Cookbook structure is complete with 4 recipes across all 4 supported languages. Each recipe includes both markdown documentation and runnable examples. diff --git a/cookbook/python/README.md b/cookbook/python/README.md index 885c8be1..120a161f 100644 --- a/cookbook/python/README.md +++ b/cookbook/python/README.md @@ -1,19 +1,60 @@ # GitHub Copilot SDK Cookbook β€” Python -This folder hosts short, practical recipes for using the GitHub Copilot SDK with Python. Each recipe is concise, copy‑pasteable, and points to fuller examples and tests. +Practical recipes for the GitHub Copilot SDK with Python. Each recipe is self-contained and ready to run. -## Recipes +## Installation **Copilot CLI:** -- [Error Handling](error-handling.md): Handle errors gracefully including connection failures, timeouts, and cleanup. -- [Multiple Sessions](multiple-sessions.md): Manage multiple independent conversations simultaneously. -- [Managing Local Files](managing-local-files.md): Organize files by metadata using AI-powered grouping strategies. -- [PR Visualization](pr-visualization.md): Generate interactive PR age charts using GitHub MCP Server. -- [Persisting Sessions](persisting-sessions.md): Save and resume sessions across restarts. + Refer to the [Getting Started guide](../docs/getting-started.md#prerequisites) for installation instructions. -## Contributing +## πŸ“š Recipes -Add a new recipe by creating a markdown file in this folder and linking it above. Follow repository guidance in [CONTRIBUTING.md](../../CONTRIBUTING.md). +| Recipe | Level | Description | +| -------- | ------- | ------------- | +| [Error Handling](error-handling.md) | Beginner | Exceptions, retries, graceful shutdown | +| [Multiple Sessions](multiple-sessions.md) | Beginner | Managing independent conversations | +| [Persisting Sessions](persisting-sessions.md) | Beginner | Save and resume sessions | +| [Managing Local Files](managing-local-files.md) | Intermediate | AI-powered file organization | +| [Streaming Responses](streaming-responses.md) | Intermediate | Real-time response streaming | +| [Custom Tools](custom-tools.md) | Intermediate | Extend Copilot with custom tools | +| [PR Visualization](pr-visualization.md) | Intermediate | Generate PR analytics charts | +| [Custom Providers](custom-providers.md) | Advanced | BYOK for custom AI providers | +| [MCP Servers](mcp-servers.md) | Advanced | Model Context Protocol integration | +| [Custom Agents](custom-agents.md) | Advanced | Build specialized assistants | -## Status +## πŸš€ Quick Start -This README is a scaffold; recipe files are placeholders until populated. +```bash +cd cookbook/python/recipe +pip install -r requirements.txt + +# Run any recipe +python error_handling.py +python pr_visualization.py --repo github/copilot-sdk +``` + +## πŸ“¦ Requirements + +- Python 3.9+ (supports up to 3.14) +- Copilot CLI installed and authenticated + +## πŸ”§ Troubleshooting + +| Issue | Solution | +| ------- | ---------- | +| `FileNotFoundError: Copilot CLI not found` | Install the Copilot CLI | +| `ConnectionError` | Check network and CLI status | +| `TimeoutError` | Increase timeout in `send_and_wait()` | + +Enable debug logging: + +```python +client = CopilotClient({"log_level": "debug"}) +``` + +## πŸ“ Contributing + +1. Add a Python file in `recipe/` +2. Add a matching `.md` file here +3. Update this README + +See [CONTRIBUTING.md](../../CONTRIBUTING.md) for details. diff --git a/cookbook/python/custom-agents.md b/cookbook/python/custom-agents.md new file mode 100644 index 00000000..713e2781 --- /dev/null +++ b/cookbook/python/custom-agents.md @@ -0,0 +1,356 @@ +# Custom Agents + +Create specialized AI agents with custom prompts and capabilities. + +> **Skill Level:** Advanced +> +> **Runnable Example:** [recipe/custom_agents.py](recipe/custom_agents.py) +> +> ```bash +> cd recipe && pip install -r requirements.txt +> python custom_agents.py +> ``` + +## Overview + +> **πŸ“– What are Custom Agents?** For an introduction to agent concepts, configuration options, and multi-language examples, see [Custom Agents Documentation](../../docs/custom-agents.md). + +This recipe covers Python-specific agent patterns: + +- Creating agents with specialized prompts +- Agent-specific tools and capabilities +- Multiple agents for different tasks +- Dynamic agent creation + +## Quick Start + +```python +import asyncio +from copilot import CopilotClient, CustomAgentConfig + +async def main(): + client = CopilotClient() + await client.start() + + # Define a code review agent + code_reviewer = CustomAgentConfig( + name="code-reviewer", + description="Expert code reviewer", + system_prompt=""" +You are an expert code reviewer specializing in Python. +Focus on: +- Code quality and readability +- Security vulnerabilities +- Performance issues +- Best practices +Always provide constructive feedback with examples. +""" + ) + + session = await client.create_session({ + "custom_agents": [code_reviewer] + }) + + await session.send_and_wait({ + "prompt": "@code-reviewer Review this function:\n\ndef add(a, b): return a+b" + }) + + await session.destroy() + await client.stop() + +asyncio.run(main()) +``` + +## Agent Patterns + +### Code Reviewer Agent + +```python +from copilot import CustomAgentConfig + +def create_code_reviewer_agent(): + """Create a specialized code review agent.""" + return CustomAgentConfig( + name="reviewer", + description="Expert code reviewer for Python projects", + system_prompt=""" +You are an expert code reviewer with deep knowledge of: +- Python best practices (PEP 8, PEP 257) +- Security vulnerabilities (OWASP Top 10) +- Performance optimization +- Design patterns + +When reviewing code: +1. Start with a brief summary +2. List issues by severity (critical, warning, suggestion) +3. Provide fixed code examples +4. End with overall assessment + +Be constructive and educational in your feedback. +""" + ) +``` + +### SQL Expert Agent + +```python +def create_sql_expert_agent(): + """Create a SQL database expert agent.""" + return CustomAgentConfig( + name="sql-expert", + description="Database and SQL expert", + system_prompt=""" +You are a database expert specializing in: +- SQL query optimization +- Schema design +- PostgreSQL, MySQL, SQLite +- Performance tuning + +When helping with queries: +1. Explain the approach +2. Provide optimized SQL +3. Note any indexes needed +4. Warn about potential issues (N+1, full table scans) + +Always consider data integrity and security. +""" + ) +``` + +### Documentation Agent + +```python +def create_docs_agent(): + """Create a documentation writer agent.""" + return CustomAgentConfig( + name="docs-writer", + description="Technical documentation expert", + system_prompt=""" +You are a technical writer specializing in: +- API documentation +- User guides +- README files +- Code comments + +When writing documentation: +1. Use clear, concise language +2. Include code examples +3. Structure with headings and lists +4. Consider the audience (beginner vs advanced) + +Follow the DiΓ‘taxis documentation framework. +""" + ) +``` + +## Multiple Agents + +Use multiple specialized agents: + +```python +async def multi_agent_demo(): + """Demonstrate multiple agents in one session.""" + client = CopilotClient() + await client.start() + + # Create multiple agents + agents = [ + create_code_reviewer_agent(), + create_sql_expert_agent(), + create_docs_agent() + ] + + session = await client.create_session({ + "custom_agents": agents + }) + + # Use specific agents by name + await session.send_and_wait({ + "prompt": "@reviewer Check this Python code for issues" + }) + + await session.send_and_wait({ + "prompt": "@sql-expert Optimize this SELECT query" + }) + + await session.send_and_wait({ + "prompt": "@docs-writer Write a README for this project" + }) + + await session.destroy() + await client.stop() +``` + +## Agents with Tools + +Add specific tools to agents: + +```python +from copilot import define_tool, CustomAgentConfig + +# Define tools for the agent +@define_tool( + name="run_linter", + description="Run a linter on Python code" +) +def run_linter(code: str) -> dict: + # In production, actually run pylint/flake8 + return {"issues": [], "score": 10.0} + +@define_tool( + name="check_security", + description="Check code for security issues" +) +def check_security(code: str) -> dict: + return {"vulnerabilities": [], "risk_level": "low"} + +# Create agent with tools +security_agent = CustomAgentConfig( + name="security-reviewer", + description="Security-focused code reviewer", + system_prompt="You are a security expert. Use the security tools." +) + +session = await client.create_session({ + "custom_agents": [security_agent], + "tools": [run_linter, check_security] +}) +``` + +## Dynamic Agent Creation + +Create agents based on context: + +```python +def create_project_agent(project_type, languages): + """Create an agent specialized for a project type.""" + + language_list = ", ".join(languages) + + prompts = { + "web": f""" +You are a web development expert specializing in {language_list}. +Focus on frontend best practices, accessibility, and performance. +""", + "api": f""" +You are a backend API expert specializing in {language_list}. +Focus on REST/GraphQL design, security, and scalability. +""", + "data": f""" +You are a data engineering expert specializing in {language_list}. +Focus on data pipelines, ETL, and analytics. +""", + "ml": f""" +You are a machine learning expert specializing in {language_list}. +Focus on model development, training, and deployment. +""" + } + + return CustomAgentConfig( + name=f"{project_type}-expert", + description=f"Expert in {project_type} development", + system_prompt=prompts.get(project_type, prompts["web"]) + ) + + +# Usage +agent = create_project_agent("api", ["Python", "FastAPI"]) +``` + +## Agent Collaboration + +Chain agents for complex tasks: + +```python +async def agent_collaboration_demo(session): + """Demonstrate agents working together.""" + + # First, get code reviewed + await session.send_and_wait({ + "prompt": "@reviewer Review this authentication code: [code]" + }) + + # Then, check security + await session.send_and_wait({ + "prompt": "@security-reviewer Analyze the security of the above code" + }) + + # Finally, document + await session.send_and_wait({ + "prompt": "@docs-writer Write API documentation for the auth endpoint" + }) +``` + +## Agent Event Handling + +Track agent interactions using `SessionEventType`: + +```python +from copilot.types import SessionEventType + +def create_agent_handler(): + """Track which agents are responding.""" + def handler(event): + if event.type == SessionEventType.SUBAGENT_SELECTED: + print(f"πŸ€– Agent: {event.data.agent_name}") + + elif event.type == SessionEventType.ASSISTANT_MESSAGE: + print(f"πŸ“ Response: {event.data.content[:100]}...") + + elif event.type == SessionEventType.TOOL_EXECUTION_START: + print(f"πŸ”§ Tool: {event.data.tool_name}") + + return handler + +session.on(create_agent_handler()) +``` + +## Agent Configuration Options + +| Option | Description | Example | +|--------|-------------|---------| +| `name` | Agent identifier | "code-reviewer" | +| `description` | What the agent does | "Reviews Python code" | +| `system_prompt` | Agent behavior/expertise | "You are an expert..." | + +## Agent Selection + +Copilot selects agents based on: + +1. **@mention**: Explicitly call `@agent-name` +2. **Context**: Agent description matches the request +3. **Default**: Falls back to general assistant + +```python +# Explicit selection +"@reviewer Check this code" + +# Implicit selection (Copilot chooses based on context) +"Review this Python function for issues" +``` + +## Best Practices + +1. **Clear system prompts**: Be specific about expertise and behavior +2. **Focused scope**: Each agent should have a clear purpose +3. **Consistent naming**: Use descriptive, memorable names +4. **Appropriate tools**: Give agents only the tools they need +5. **Test prompts**: Iterate on system prompts for best results + +## Complete Example + +```bash +python recipe/custom_agents.py +``` + +Demonstrates: +- Code reviewer agent +- SQL expert agent +- Multiple agent sessions +- Dynamic agent creation + +## Next Steps + +- [Custom Tools](custom-tools.md): Add tools to agents +- [MCP Servers](mcp-servers.md): Extend agent capabilities +- [Custom Providers](custom-providers.md): Use different models for agents diff --git a/cookbook/python/custom-providers.md b/cookbook/python/custom-providers.md new file mode 100644 index 00000000..71d25408 --- /dev/null +++ b/cookbook/python/custom-providers.md @@ -0,0 +1,339 @@ +# Custom Providers (BYOK) + +Configure custom model providers with your own API keys. + +> **Skill Level:** Advanced +> +> **Runnable Example:** [recipe/custom_providers.py](recipe/custom_providers.py) +> +> ```bash +> cd recipe && pip install -r requirements.txt +> python custom_providers.py +> ``` + +## Overview + +This recipe covers Bring Your Own Key (BYOK) patterns: + +- OpenAI API configuration +- Azure OpenAI integration +- Anthropic Claude configuration +- Custom endpoints +- Provider fallback patterns + +## Quick Start + +```python +import asyncio +import os +from copilot import CopilotClient, ProviderConfig + +async def main(): + # Configure OpenAI provider + provider = ProviderConfig( + type="openai", + api_key=os.environ["OPENAI_API_KEY"], + model="gpt-4o" + ) + + client = CopilotClient() + await client.start() + + session = await client.create_session({ + "provider": provider + }) + + await session.send_and_wait({ + "prompt": "Hello from custom provider!" + }) + + await session.destroy() + await client.stop() + +asyncio.run(main()) +``` + +## Provider Configurations + +### OpenAI + +```python +from copilot import ProviderConfig + +openai_provider = ProviderConfig( + type="openai", + api_key=os.environ["OPENAI_API_KEY"], + model="gpt-4o", # or "gpt-4-turbo", "gpt-3.5-turbo" + base_url=None # Optional: custom endpoint +) +``` + +### Azure OpenAI + +```python +azure_provider = ProviderConfig( + type="azure", + api_key=os.environ["AZURE_OPENAI_API_KEY"], + base_url=os.environ["AZURE_OPENAI_ENDPOINT"], # e.g., "https://your-resource.openai.azure.com" + model="gpt-4o", # Your deployment name + api_version="2024-02-15-preview" # Optional +) +``` + +### Anthropic Claude + +```python +anthropic_provider = ProviderConfig( + type="anthropic", + api_key=os.environ["ANTHROPIC_API_KEY"], + model="claude-sonnet-4-20250514" # or "claude-3-opus-20240229" +) +``` + +### Custom Endpoint + +```python +custom_provider = ProviderConfig( + type="openai", # Use OpenAI-compatible format + api_key=os.environ["CUSTOM_API_KEY"], + base_url="https://your-custom-endpoint.com/v1", + model="your-model-name" +) +``` + +## Helper Functions + +Create provider helpers for cleaner code: + +```python +def get_openai_provider(model="gpt-4o"): + """Get OpenAI provider configuration.""" + api_key = os.environ.get("OPENAI_API_KEY") + if not api_key: + raise ValueError("OPENAI_API_KEY not set") + + return ProviderConfig( + type="openai", + api_key=api_key, + model=model + ) + + +def get_azure_provider(deployment_name="gpt-4o"): + """Get Azure OpenAI provider configuration.""" + api_key = os.environ.get("AZURE_OPENAI_API_KEY") + endpoint = os.environ.get("AZURE_OPENAI_ENDPOINT") + + if not api_key or not endpoint: + raise ValueError("AZURE_OPENAI_API_KEY and AZURE_OPENAI_ENDPOINT required") + + return ProviderConfig( + type="azure", + api_key=api_key, + base_url=endpoint, + model=deployment_name + ) + + +def get_anthropic_provider(model="claude-sonnet-4-20250514"): + """Get Anthropic provider configuration.""" + api_key = os.environ.get("ANTHROPIC_API_KEY") + if not api_key: + raise ValueError("ANTHROPIC_API_KEY not set") + + return ProviderConfig( + type="anthropic", + api_key=api_key, + model=model + ) +``` + +## Provider Selection + +Choose providers based on task: + +```python +async def select_provider_for_task(task_type): + """Select the best provider for a given task.""" + providers = { + "code": get_openai_provider("gpt-4o"), + "creative": get_anthropic_provider("claude-sonnet-4-20250514"), + "fast": get_openai_provider("gpt-4o-mini"), + "enterprise": get_azure_provider("gpt-4o"), + } + + return providers.get(task_type, providers["code"]) + + +# Usage +client = CopilotClient() +await client.start() + +provider = await select_provider_for_task("code") +session = await client.create_session({"provider": provider}) +``` + +## Provider Fallback + +Implement fallback when primary provider fails: + +```python +async def create_session_with_fallback(client, primary, fallbacks): + """Create session with provider fallback.""" + providers = [primary] + fallbacks + + for i, provider in enumerate(providers): + try: + session = await client.create_session({"provider": provider}) + + # Test the connection + await session.send_and_wait({ + "prompt": "ping" + }, timeout=10.0) + + print(f"Using provider {i + 1}: {provider.type}") + return session + + except Exception as e: + print(f"Provider {i + 1} failed: {e}") + if i < len(providers) - 1: + print("Trying next provider...") + continue + + raise RuntimeError("All providers failed") + + +# Usage +session = await create_session_with_fallback( + client, + primary=get_openai_provider(), + fallbacks=[ + get_azure_provider(), + get_anthropic_provider() + ] +) +``` + +## Multiple Providers + +Use different providers in the same application: + +```python +async def multi_provider_demo(): + """Demonstrate using multiple providers.""" + client = CopilotClient() + await client.start() + + # OpenAI for code tasks + code_session = await client.create_session({ + "session_id": "code-assistant", + "provider": get_openai_provider("gpt-4o") + }) + + # Claude for analysis + analysis_session = await client.create_session({ + "session_id": "analysis-assistant", + "provider": get_anthropic_provider() + }) + + # Use each for their strengths + await code_session.send_and_wait({ + "prompt": "Write a Python function to parse JSON" + }) + + await analysis_session.send_and_wait({ + "prompt": "Analyze the security implications of parsing untrusted JSON" + }) + + await code_session.destroy() + await analysis_session.destroy() + await client.stop() +``` + +## Environment Setup + +Recommended environment variables: + +```bash +# .env file +OPENAI_API_KEY=sk-... +AZURE_OPENAI_API_KEY=... +AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com +ANTHROPIC_API_KEY=sk-ant-... +``` + +Load with python-dotenv: + +```python +from dotenv import load_dotenv +load_dotenv() +``` + +## Provider Comparison + +| Provider | Best For | Models | +|----------|----------|--------| +| OpenAI | General use, code | gpt-4o, gpt-4o-mini | +| Azure OpenAI | Enterprise, compliance | Same as OpenAI | +| Anthropic | Analysis, safety | claude-3-opus, claude-sonnet-4 | + +## Error Handling + +Handle provider-specific errors: + +```python +async def safe_provider_request(session, prompt): + """Handle provider-specific errors.""" + try: + await session.send_and_wait({"prompt": prompt}) + + except TimeoutError: + print("Request timed out - provider may be slow") + + except Exception as e: + error_msg = str(e).lower() + + if "rate limit" in error_msg: + print("Rate limited - waiting before retry") + await asyncio.sleep(60) + + elif "invalid api key" in error_msg: + print("Invalid API key - check configuration") + + elif "model not found" in error_msg: + print("Model not available - check model name") + + else: + print(f"Provider error: {e}") + + raise +``` + +## Best Practices + +1. **Secure API keys**: Use environment variables, never commit keys +2. **Implement fallbacks**: Have backup providers ready +3. **Match provider to task**: Use the right model for the job +4. **Handle rate limits**: Implement retry with backoff +5. **Monitor costs**: Different providers have different pricing + +## Complete Example + +```bash +# Set environment variables first +export OPENAI_API_KEY=sk-... + +python recipe/custom_providers.py +``` + +Demonstrates: +- Multiple provider configurations +- Provider selection +- Fallback patterns +- Multi-provider applications + +## Next Steps + +- [Custom Agents](custom-agents.md): Use providers with custom agents +- [Error Handling](error-handling.md): Handle provider errors +- [MCP Servers](mcp-servers.md): Combine providers with MCP tools diff --git a/cookbook/python/custom-tools.md b/cookbook/python/custom-tools.md new file mode 100644 index 00000000..9071f688 --- /dev/null +++ b/cookbook/python/custom-tools.md @@ -0,0 +1,347 @@ +# Custom Tools + +Create custom tools to extend Copilot's capabilities with your own functionality. + +> **Skill Level:** Intermediate to Advanced +> +> **Runnable Example:** [recipe/custom_tools.py](recipe/custom_tools.py) +> +> ```bash +> cd recipe && pip install -r requirements.txt +> python custom_tools.py +> ``` + +## Overview + +This recipe covers custom tool development: + +- Basic tool definition with `@define_tool` +- Pydantic models for parameter validation +- Async handlers for non-blocking operations +- Structured results with `ToolResult` +- Tool orchestration patterns + +## Quick Start + +```python +import asyncio +from copilot import CopilotClient, define_tool + +# Define a simple tool +@define_tool( + name="get_weather", + description="Get the current weather for a location" +) +def get_weather(location: str) -> str: + # In production, call a real weather API + return f"Weather in {location}: 72Β°F, Sunny" + +async def main(): + client = CopilotClient() + await client.start() + + session = await client.create_session({ + "tools": [get_weather] # Register the tool + }) + + await session.send_and_wait({ + "prompt": "What's the weather in San Francisco?" + }) + # Copilot will call your get_weather tool! + + await session.destroy() + await client.stop() + +asyncio.run(main()) +``` + +## Tool Definition Patterns + +### Basic Tool + +Simple function with type hints: + +```python +@define_tool( + name="calculate_tax", + description="Calculate sales tax for an amount" +) +def calculate_tax(amount: float, rate: float = 0.0825) -> float: + """Calculate tax. Rate defaults to 8.25%.""" + return round(amount * rate, 2) +``` + +### Pydantic Model Parameters + +Use Pydantic for complex input validation: + +```python +from pydantic import BaseModel, Field +from typing import Literal + +class CreateTicketParams(BaseModel): + title: str = Field(description="Ticket title") + description: str = Field(description="Detailed description") + priority: Literal["low", "medium", "high"] = Field( + default="medium", + description="Ticket priority" + ) + assignee: str | None = Field(default=None) + +@define_tool( + name="create_ticket", + description="Create a support ticket in the system" +) +def create_ticket(params: CreateTicketParams) -> dict: + return { + "id": "TICKET-123", + "title": params.title, + "priority": params.priority, + "status": "created" + } +``` + +### Async Handlers + +For I/O-bound operations: + +```python +import aiohttp + +@define_tool( + name="fetch_api", + description="Fetch data from an API endpoint" +) +async def fetch_api(url: str, method: str = "GET") -> dict: + async with aiohttp.ClientSession() as session: + async with session.request(method, url) as response: + return { + "status": response.status, + "data": await response.json() + } +``` + +## Structured Results + +Use `ToolResult` for rich responses: + +```python +from copilot import ToolResult + +@define_tool( + name="analyze_code", + description="Analyze code for issues" +) +def analyze_code(code: str, language: str) -> ToolResult: + issues = [ + {"line": 5, "severity": "warning", "message": "Unused variable"}, + {"line": 12, "severity": "error", "message": "Syntax error"} + ] + + return ToolResult( + content=f"Found {len(issues)} issues", + structured_data={"issues": issues, "language": language}, + is_error=any(i["severity"] == "error" for i in issues) + ) +``` + +## Multiple Tools + +Register multiple tools together: + +```python +# Define tools +@define_tool(name="search_docs", description="Search documentation") +def search_docs(query: str) -> list: + return ["Result 1", "Result 2"] + +@define_tool(name="get_examples", description="Get code examples") +def get_examples(topic: str) -> list: + return [f"Example for {topic}"] + +@define_tool(name="run_tests", description="Run test suite") +async def run_tests(test_path: str) -> dict: + return {"passed": 10, "failed": 0} + +# Register all tools +session = await client.create_session({ + "tools": [search_docs, get_examples, run_tests] +}) +``` + +## Tool Categories + +### Database Tools + +```python +@define_tool( + name="query_database", + description="Execute a SQL query" +) +async def query_database(query: str, database: str = "main") -> dict: + # Use async database driver + return {"rows": [], "count": 0} + +@define_tool( + name="insert_record", + description="Insert a record into a table" +) +async def insert_record(table: str, data: dict) -> dict: + return {"id": 1, "success": True} +``` + +### File System Tools + +```python +import os + +@define_tool( + name="list_files", + description="List files in a directory" +) +def list_files(path: str, pattern: str = "*") -> list: + import glob + return glob.glob(os.path.join(path, pattern)) + +@define_tool( + name="read_file_info", + description="Get file metadata" +) +def read_file_info(path: str) -> dict: + stat = os.stat(path) + return { + "size": stat.st_size, + "modified": stat.st_mtime, + "is_directory": os.path.isdir(path) + } +``` + +### HTTP Tools + +```python +@define_tool( + name="http_request", + description="Make an HTTP request" +) +async def http_request( + url: str, + method: str = "GET", + headers: dict = None, + body: dict = None +) -> dict: + import aiohttp + + async with aiohttp.ClientSession() as session: + async with session.request( + method, url, + headers=headers, + json=body + ) as response: + return { + "status": response.status, + "headers": dict(response.headers), + "body": await response.text() + } +``` + +## Event Handling + +Monitor tool execution: + +```python +from copilot.types import SessionEventType + +def create_tool_handler(): + """Track tool execution events.""" + def handler(event): + if event.type == SessionEventType.TOOL_EXECUTION_START: + print(f"πŸ”§ Starting: {event.data.tool_name}") + print(f" Args: {event.data.arguments}") + + elif event.type == SessionEventType.TOOL_EXECUTION_COMPLETE: + print(f"βœ… Completed") + + elif event.type == SessionEventType.SESSION_ERROR: + print(f"❌ Error: {event.data.message}") + + return handler + +session.on(create_tool_handler()) +``` + +## Error Handling + +Handle tool errors gracefully: + +```python +@define_tool( + name="risky_operation", + description="An operation that might fail" +) +def risky_operation(input_data: str) -> ToolResult: + try: + result = process(input_data) + return ToolResult( + content=f"Success: {result}", + is_error=False + ) + except ValueError as e: + return ToolResult( + content=f"Invalid input: {e}", + is_error=True + ) + except Exception as e: + return ToolResult( + content=f"Unexpected error: {e}", + is_error=True + ) +``` + +## Tool Orchestration + +Combine multiple tools in a workflow: + +```python +async def workflow_demo(session): + """Demonstrate tool orchestration.""" + + # Single prompt triggers multiple tool calls + await session.send_and_wait({ + "prompt": """ +1. Search the documentation for 'authentication' +2. Get code examples for the results +3. Run the test suite for auth tests +4. Summarize the findings +""" + }) + # Copilot will call search_docs β†’ get_examples β†’ run_tests +``` + +## Best Practices + +| Practice | Description | +|----------|-------------| +| Clear names | Use descriptive, action-oriented names | +| Good descriptions | Help Copilot understand when to use the tool | +| Type hints | Always include type annotations | +| Pydantic models | Use for complex parameters | +| Error handling | Return ToolResult with is_error=True | +| Async for I/O | Use async for network/file operations | + +## Complete Example + +```bash +python recipe/custom_tools.py +``` + +Demonstrates: +- Basic and advanced tool definitions +- Pydantic parameter validation +- Async handlers +- Tool orchestration + +## Next Steps + +- [MCP Servers](mcp-servers.md): Use external tool servers +- [Custom Agents](custom-agents.md): Create specialized agents with tools +- [Streaming Responses](streaming-responses.md): Stream tool results diff --git a/cookbook/python/error-handling.md b/cookbook/python/error-handling.md index 63d1488d..da219387 100644 --- a/cookbook/python/error-handling.md +++ b/cookbook/python/error-handling.md @@ -1,150 +1,264 @@ # Error Handling Patterns -Handle errors gracefully in your Copilot SDK applications. +Master error handling in your Copilot SDK applications with production-ready patterns. -> **Runnable example:** [recipe/error_handling.py](recipe/error_handling.py) +> **Skill Level:** Beginner to Intermediate +> +> **Runnable Example:** [recipe/error_handling.py](recipe/error_handling.py) > > ```bash > cd recipe && pip install -r requirements.txt > python error_handling.py > ``` -## Example scenario +## Overview + +This recipe covers essential error handling patterns for building robust applications with the Copilot SDK: + +- Basic try-except-finally patterns +- Context managers for automatic cleanup +- Retry logic with exponential backoff +- Timeout handling and request abortion +- Graceful shutdown with signal handling -You need to handle various error conditions like connection failures, timeouts, and invalid responses. +## Quick Start -## Basic try-except +The simplest error handling pattern: ```python +import asyncio from copilot import CopilotClient -client = CopilotClient() +async def main(): + client = CopilotClient() -try: - client.start() - session = client.create_session(model="gpt-5") - - response = None - def handle_message(event): - nonlocal response - if event["type"] == "assistant.message": - response = event["data"]["content"] - - session.on(handle_message) - session.send(prompt="Hello!") - session.wait_for_idle() - - if response: - print(response) - - session.destroy() -except Exception as e: - print(f"Error: {e}") -finally: - client.stop() + try: + await client.start() + session = await client.create_session() + + response = await session.send_and_wait( + {"prompt": "Hello!"}, + timeout=30.0 + ) + + await session.destroy() + + except FileNotFoundError: + print("Copilot CLI not found. Please install it.") + except ConnectionError: + print("Could not connect to server.") + except asyncio.TimeoutError: + print("Request timed out.") + except Exception as e: + print(f"Error: {e}") + finally: + await client.stop() + +asyncio.run(main()) ``` -## Handling specific error types +## Error Types -```python -import subprocess +### Common Exceptions + +| Exception | Cause | Solution | +|-----------|-------|----------| +| `FileNotFoundError` | CLI not installed | Install Copilot CLI | +| `ConnectionError` | Network issues | Check connection | +| `asyncio.TimeoutError` | Request took too long | Increase timeout | +| `RuntimeError` | Protocol mismatch | Update SDK/CLI | + +### Handling Specific Errors +```python try: - client.start() + await client.start() except FileNotFoundError: - print("Copilot CLI not found. Please install it first.") -except ConnectionError: - print("Could not connect to Copilot CLI server.") -except Exception as e: - print(f"Unexpected error: {e}") + print("Install: https://github.com/github/copilot-cli") +except ConnectionError as e: + print(f"Connection failed: {e}") +except RuntimeError as e: + if "protocol version" in str(e).lower(): + print("Update your SDK or CLI to match versions.") + else: + raise ``` -## Timeout handling +## Context Manager Pattern (Recommended) -```python -import signal -from contextlib import contextmanager +Create a reusable context manager for automatic cleanup: -@contextmanager -def timeout(seconds): - def timeout_handler(signum, frame): - raise TimeoutError("Request timed out") +```python +class CopilotContext: + """Automatic cleanup with context manager.""" + + def __init__(self, **options): + self.client = CopilotClient(options if options else None) + self.session = None + + async def __aenter__(self): + await self.client.start() + self.session = await self.client.create_session() + return self.client, self.session + + async def __aexit__(self, exc_type, exc_val, exc_tb): + if self.session: + try: + await self.session.destroy() + except Exception: + pass + await self.client.stop() + return False # Don't suppress exceptions + + +# Usage +async def main(): + async with CopilotContext() as (client, session): + await session.send_and_wait({"prompt": "Hello!"}) + # Automatic cleanup happens here +``` - old_handler = signal.signal(signal.SIGALRM, timeout_handler) - signal.alarm(seconds) - try: - yield - finally: - signal.alarm(0) - signal.signal(signal.SIGALRM, old_handler) +## Timeout Handling -session = client.create_session(model="gpt-5") +### Using send_and_wait with Timeout +```python try: - session.send(prompt="Complex question...") - - # Wait with timeout (30 seconds) - with timeout(30): - session.wait_for_idle() - - print("Response received") -except TimeoutError: - print("Request timed out") + # Wait up to 60 seconds + response = await session.send_and_wait( + {"prompt": "Complex question..."}, + timeout=60.0 + ) +except asyncio.TimeoutError: + print("Request timed out after 60 seconds") ``` -## Aborting a request +### Aborting Long Requests ```python -import threading +async def abort_after_delay(session, seconds): + await asyncio.sleep(seconds) + await session.abort() + print(f"Aborted after {seconds}s") -session = client.create_session(model="gpt-5") +# Start abort task and request concurrently +abort_task = asyncio.create_task(abort_after_delay(session, 10)) -# Start a request -session.send(prompt="Write a very long story...") +try: + await session.send({"prompt": "Long task..."}) + await abort_task +except Exception: + pass +``` + +## Retry Pattern -# Abort it after some condition -def abort_later(): - import time - time.sleep(5) - session.abort() - print("Request aborted") +Implement exponential backoff for transient failures: -threading.Thread(target=abort_later).start() +```python +async def retry_with_backoff( + func, + max_retries=3, + base_delay=1.0, + max_delay=30.0, +): + """Retry with exponential backoff.""" + for attempt in range(max_retries + 1): + try: + return await func() + except (ConnectionError, asyncio.TimeoutError) as e: + if attempt < max_retries: + delay = min(base_delay * (2 ** attempt), max_delay) + print(f"Retry {attempt + 1}/{max_retries} in {delay}s...") + await asyncio.sleep(delay) + else: + raise + + +# Usage +response = await retry_with_backoff( + lambda: session.send_and_wait({"prompt": "Hello!"}, timeout=30.0), + max_retries=3 +) ``` -## Graceful shutdown +## Graceful Shutdown + +Handle Ctrl+C and SIGTERM gracefully: ```python import signal import sys -def signal_handler(sig, frame): - print("\nShutting down...") - errors = client.stop() - if errors: - print(f"Cleanup errors: {errors}") - sys.exit(0) +class GracefulShutdown: + def __init__(self, client): + self.client = client + self.shutdown_event = asyncio.Event() + + def register(self): + loop = asyncio.get_running_loop() + + def handler(sig): + print(f"\nReceived {sig.name}, shutting down...") + self.shutdown_event.set() + + if sys.platform != "win32": + for sig in (signal.SIGINT, signal.SIGTERM): + loop.add_signal_handler(sig, lambda s=sig: handler(s)) + else: + signal.signal(signal.SIGINT, lambda s, f: handler(signal.SIGINT)) + + async def wait(self): + await self.shutdown_event.wait() + + async def cleanup(self): + errors = await self.client.stop() + if errors: + print(f"Cleanup errors: {errors}") -signal.signal(signal.SIGINT, signal_handler) + +# Usage in a long-running application +async def main(): + client = CopilotClient() + shutdown = GracefulShutdown(client) + + try: + await client.start() + shutdown.register() + + # Your application loop + while not shutdown.shutdown_event.is_set(): + await asyncio.sleep(0.1) + + finally: + await shutdown.cleanup() ``` -## Context manager for automatic cleanup +## Best Practices -```python -from copilot import CopilotClient +1. **Always clean up**: Use try-finally or context managers +2. **Set appropriate timeouts**: Match timeout to expected task duration +3. **Implement retries**: Handle transient network failures +4. **Log errors**: Capture details for debugging +5. **Validate early**: Check prerequisites before starting -with CopilotClient() as client: - client.start() - session = client.create_session(model="gpt-5") +## Complete Example - # ... do work ... +See the full implementation with all patterns: - # client.stop() is automatically called when exiting context +```bash +python recipe/error_handling.py ``` -## Best practices +The example demonstrates: +- Basic error handling +- Context manager pattern +- Retry with exponential backoff +- Timeout and abort patterns +- Graceful shutdown + +## Next Steps -1. **Always clean up**: Use try-finally or context managers to ensure `stop()` is called -2. **Handle connection errors**: The CLI might not be installed or running -3. **Set appropriate timeouts**: Long-running requests should have timeouts -4. **Log errors**: Capture error details for debugging +- [Multiple Sessions](multiple-sessions.md): Manage concurrent conversations +- [Persisting Sessions](persisting-sessions.md): Save and resume sessions +- [Custom Tools](custom-tools.md): Extend Copilot with your own functions diff --git a/cookbook/python/managing-local-files.md b/cookbook/python/managing-local-files.md index a085c538..d8d17f21 100644 --- a/cookbook/python/managing-local-files.md +++ b/cookbook/python/managing-local-files.md @@ -1,119 +1,273 @@ -# Grouping Files by Metadata +# Managing Local Files -Use Copilot to intelligently organize files in a folder based on their metadata. +Use Copilot to intelligently organize files based on metadata and content. -> **Runnable example:** [recipe/managing_local_files.py](recipe/managing_local_files.py) +> **Skill Level:** Beginner to Advanced +> +> **Runnable Example:** [recipe/managing_local_files.py](recipe/managing_local_files.py) > > ```bash > cd recipe && pip install -r requirements.txt > python managing_local_files.py > ``` -## Example scenario +## Overview + +This recipe demonstrates AI-powered file organization: -You have a folder with many files and want to organize them into subfolders based on metadata like file type, creation date, size, or other attributes. Copilot can analyze the files and suggest or execute a grouping strategy. +- Multiple organization strategies (extension, date, size, smart) +- Permission handling for file operations +- Interactive mode for user confirmation +- Dry-run mode for preview without changes -## Example code +## Quick Start ```python -from copilot import CopilotClient +import asyncio import os +from copilot import CopilotClient +from copilot.types import SessionEventType -# Create and start client -client = CopilotClient() -client.start() +async def main(): + client = CopilotClient() + await client.start() -# Create session -session = client.create_session(model="gpt-5") + session = await client.create_session() -# Event handler -def handle_event(event): - if event["type"] == "assistant.message": - print(f"\nCopilot: {event['data']['content']}") - elif event["type"] == "tool.execution_start": - print(f" β†’ Running: {event['data']['toolName']}") - elif event["type"] == "tool.execution_complete": - print(f" βœ“ Completed: {event['data']['toolCallId']}") + # Event handler for visibility + def handle_event(event): + if event.type == SessionEventType.ASSISTANT_MESSAGE: + print(f"\nCopilot: {event.data.content}") + elif event.type == SessionEventType.TOOL_EXECUTION_START: + print(f" β†’ Running: {event.data.tool_name}") + elif event.type == SessionEventType.TOOL_EXECUTION_COMPLETE: + print(f" βœ“ Completed") -session.on(handle_event) + session.on(handle_event) -# Ask Copilot to organize files -target_folder = os.path.expanduser("~/Downloads") + # Organize files + target = os.path.expanduser("~/Downloads") -session.send(prompt=f""" -Analyze the files in "{target_folder}" and organize them into subfolders. + await session.send_and_wait({ + "prompt": f""" +Analyze the files in "{target}" and organize them into subfolders by type: -1. First, list all files and their metadata -2. Preview grouping by file extension -3. Create appropriate subfolders (e.g., "images", "documents", "videos") -4. Move each file to its appropriate subfolder +1. List all files and their metadata +2. Group by extension (images, documents, videos, etc.) +3. Create appropriate subfolders +4. Move files to their categories Please confirm before moving any files. -""") +""" + }) -session.wait_for_idle() + await session.destroy() + await client.stop() -client.stop() +asyncio.run(main()) ``` -## Grouping strategies +## Organization Strategies -### By file extension +### By File Extension ```python -# Groups files like: -# images/ -> .jpg, .png, .gif -# documents/ -> .pdf, .docx, .txt -# videos/ -> .mp4, .avi, .mov +await session.send_and_wait({ + "prompt": f"Organize files in '{folder}' by extension into categories like images, documents, videos" +}) + +# Result: +# images/ -> .jpg, .png, .gif, .webp +# documents/ -> .pdf, .docx, .txt, .xlsx +# videos/ -> .mp4, .avi, .mov, .mkv +# audio/ -> .mp3, .wav, .flac +# code/ -> .py, .js, .ts, .cpp ``` -### By creation date +### By Date ```python -# Groups files like: -# 2024-01/ -> files created in January 2024 -# 2024-02/ -> files created in February 2024 +await session.send_and_wait({ + "prompt": f"Organize files in '{folder}' by creation date into monthly folders" +}) + +# Result: +# 2024-01/ -> files from January 2024 +# 2024-02/ -> files from February 2024 ``` -### By file size +### By Size ```python -# Groups files like: +await session.send_and_wait({ + "prompt": f"Organize files in '{folder}' by size: tiny (<1KB), small (<1MB), medium (<100MB), large (>100MB)" +}) + +# Result: # tiny-under-1kb/ # small-under-1mb/ # medium-under-100mb/ # large-over-100mb/ ``` -## Dry-run mode +### Smart Organization + +Let AI determine the best organization: + +```python +await session.send_and_wait({ + "prompt": f""" +Analyze files in '{folder}' and suggest a logical organization based on: +- File names and content hints +- File types and typical uses +- Date patterns suggesting projects or events + +Propose descriptive folder names. +""" +}) +``` + +## Permission Handling + +Control what file operations are allowed: + +```python +def create_permission_handler(mode="confirm"): + """Create permission handler for file operations.""" + def handler(event): + if event.type != "permission.requested": + return None + + permission = event.data.permission_type + resource = event.data.resource + + if mode == "allow-all": + return True + elif mode == "deny-writes": + if permission in ["write", "delete", "move"]: + print(f"Denied: {permission} on {resource}") + return False + return True + elif mode == "confirm": + print(f"\nPermission requested: {permission}") + print(f" Resource: {resource}") + response = input(" Allow? (y/n): ").lower() + return response == 'y' + + return False + + return handler + +# Usage +session.on(create_permission_handler(mode="confirm")) +``` + +## Dry-Run Mode + +Preview changes without executing: + +```python +await session.send_and_wait({ + "prompt": f""" +Analyze files in '{folder}' and show me how you would organize them. +DO NOT move any files - just show me the plan in a table format: + +| Current Path | Proposed Folder | Reason | +""" +}) +``` + +## Interactive Mode + +Get user confirmation for each action: + +```python +async def interactive_organize(session, folder, strategy="extension"): + """Interactive file organization with confirmations.""" + + # Step 1: Analyze + await session.send_and_wait({ + "prompt": f"List all files in '{folder}' with their metadata (size, date, type)" + }) + + # Step 2: Propose + await session.send_and_wait({ + "prompt": f"Propose an organization by {strategy}. Show in a table." + }) + + # Step 3: Confirm + confirm = input("\nProceed with organization? (y/n): ") + if confirm.lower() != 'y': + print("Cancelled.") + return + + # Step 4: Execute + await session.send_and_wait({ + "prompt": "Execute the proposed organization. Report progress." + }) +``` + +## File Filtering -For safety, you can ask Copilot to only preview changes: +Organize specific file types only: ```python -session.send(prompt=f""" -Analyze files in "{target_folder}" and show me how you would organize them -by file type. DO NOT move any files - just show me the plan. -""") +await session.send_and_wait({ + "prompt": f""" +In '{folder}', organize ONLY image files (.jpg, .png, .gif): +- By resolution: small (<500px), medium (<2000px), large (>2000px) +- Skip non-image files +""" +}) ``` -## Custom grouping with AI analysis +## Duplicate Handling -Let Copilot determine the best grouping based on file content: +Handle files with the same name: ```python -session.send(prompt=f""" -Look at the files in "{target_folder}" and suggest a logical organization. -Consider: -- File names and what they might contain -- File types and their typical uses -- Date patterns that might indicate projects or events - -Propose folder names that are descriptive and useful. -""") +await session.send_and_wait({ + "prompt": f""" +Organize files in '{folder}' by type. When duplicates exist: +- Add a numeric suffix (file_1.txt, file_2.txt) +- Keep the newest version in the main folder +- Report all duplicates found +""" +}) ``` -## Safety considerations +## Safety Considerations + +| Concern | Solution | +|---------|----------| +| Accidental deletion | Use dry-run first | +| Permission errors | Set up permission handler | +| Duplicate names | Add suffix or skip | +| Important files | Copy instead of move | +| Undo capability | Log all operations | + +## Best Practices + +1. **Always dry-run first**: Preview changes before executing +2. **Use permission handlers**: Control what operations are allowed +3. **Back up important files**: Copy instead of move for critical data +4. **Log operations**: Keep a record of what was moved where +5. **Confirm before bulk operations**: Especially for delete operations + +## Complete Example + +```bash +python recipe/managing_local_files.py +``` + +Demonstrates: +- All organization strategies +- Permission handling +- Interactive mode +- Dry-run preview + +## Next Steps -1. **Confirm before moving**: Ask Copilot to confirm before executing moves -2. **Handle duplicates**: Consider what happens if a file with the same name exists -3. **Preserve originals**: Consider copying instead of moving for important files +- [Error Handling](error-handling.md): Handle file operation errors +- [Custom Tools](custom-tools.md): Create specialized file tools +- [Multiple Sessions](multiple-sessions.md): Parallel file processing diff --git a/cookbook/python/mcp-servers.md b/cookbook/python/mcp-servers.md new file mode 100644 index 00000000..63d058c4 --- /dev/null +++ b/cookbook/python/mcp-servers.md @@ -0,0 +1,331 @@ +# MCP Servers + +Configure and use Model Context Protocol (MCP) servers for extended capabilities. + +> **Skill Level:** Advanced +> +> **Runnable Example:** [recipe/mcp_servers.py](recipe/mcp_servers.py) +> +> ```bash +> cd recipe && pip install -r requirements.txt +> python mcp_servers.py +> ``` + +## Overview + +> **πŸ“– What is MCP?** For an introduction to MCP concepts, server types, and configuration options, see [MCP Documentation](../../docs/mcp.md). + +This recipe covers Python-specific MCP patterns: + +- GitHub MCP server configuration +- Filesystem MCP server setup +- Custom MCP servers +- Tool filtering and configuration + +## Quick Start + +```python +import asyncio +from copilot import CopilotClient, MCPServerConfig + +async def main(): + # Configure GitHub MCP server + github_mcp = MCPServerConfig( + name="github", + command="npx", + args=["-y", "@modelcontextprotocol/server-github"], + env={"GITHUB_TOKEN": os.environ["GITHUB_TOKEN"]} + ) + + client = CopilotClient() + await client.start() + + session = await client.create_session({ + "mcp_servers": [github_mcp] + }) + + # Copilot now has access to GitHub tools! + await session.send_and_wait({ + "prompt": "List the open issues in my-org/my-repo" + }) + + await session.destroy() + await client.stop() + +asyncio.run(main()) +``` + +## GitHub MCP Server + +Full configuration for GitHub operations: + +```python +import os +from copilot import MCPServerConfig + +def get_github_mcp_server(): + """Configure GitHub MCP server with token.""" + token = os.environ.get("GITHUB_TOKEN") + if not token: + raise ValueError("GITHUB_TOKEN environment variable required") + + return MCPServerConfig( + name="github", + command="npx", + args=["-y", "@modelcontextprotocol/server-github"], + env={ + "GITHUB_TOKEN": token + } + ) +``` + +### GitHub Capabilities + +The GitHub MCP server provides: + +| Tool | Description | +|------|-------------| +| `list_issues` | List repository issues | +| `create_issue` | Create new issues | +| `list_pull_requests` | List PRs | +| `create_pull_request` | Create new PRs | +| `get_file_contents` | Read file from repo | +| `search_repositories` | Search GitHub | + +### Usage Example + +```python +await session.send_and_wait({ + "prompt": """ + For the repository 'owner/repo': + 1. List all open issues labeled 'bug' + 2. Show the 5 most recent pull requests + 3. Get the contents of README.md + """ +}) +``` + +## Filesystem MCP Server + +Access local files safely: + +```python +def get_filesystem_mcp_server(allowed_paths): + """Configure filesystem MCP server with allowed paths.""" + return MCPServerConfig( + name="filesystem", + command="npx", + args=[ + "-y", + "@modelcontextprotocol/server-filesystem", + *allowed_paths # Directories the server can access + ] + ) + + +# Usage +fs_mcp = get_filesystem_mcp_server([ + "/home/user/projects", + "/home/user/documents" +]) + +session = await client.create_session({ + "mcp_servers": [fs_mcp] +}) +``` + +### Filesystem Capabilities + +| Tool | Description | +|------|-------------| +| `read_file` | Read file contents | +| `write_file` | Write to files | +| `list_directory` | List directory contents | +| `create_directory` | Create directories | +| `move_file` | Move/rename files | +| `search_files` | Search by pattern | + +## Multiple MCP Servers + +Combine multiple MCP servers: + +```python +async def multi_mcp_demo(): + """Use multiple MCP servers together.""" + + github_mcp = get_github_mcp_server() + filesystem_mcp = get_filesystem_mcp_server(["/home/user/projects"]) + + session = await client.create_session({ + "mcp_servers": [github_mcp, filesystem_mcp] + }) + + # Copilot can use tools from both servers + await session.send_and_wait({ + "prompt": """ + 1. Get the README from github/owner/repo + 2. Save it to /home/user/projects/readme-backup.md + """ + }) +``` + +## Tool Filtering + +Control which MCP tools are available: + +```python +# Allow only specific tools +github_mcp = MCPServerConfig( + name="github", + command="npx", + args=["-y", "@modelcontextprotocol/server-github"], + env={"GITHUB_TOKEN": token}, + # Only expose read operations + allowed_tools=["list_issues", "list_pull_requests", "get_file_contents"] +) + +# Or block specific tools +github_mcp = MCPServerConfig( + name="github", + command="npx", + args=["-y", "@modelcontextprotocol/server-github"], + env={"GITHUB_TOKEN": token}, + # Block write operations + blocked_tools=["create_issue", "create_pull_request", "delete_file"] +) +``` + +## Custom MCP Servers + +Create your own MCP server: + +```python +# Your custom MCP server (server.py) +from mcp import Server, Tool + +server = Server("my-tools") + +@server.tool("get_database_stats") +async def get_database_stats(database: str) -> dict: + """Get statistics for a database.""" + return {"tables": 10, "rows": 1000} + +# Configure in SDK +custom_mcp = MCPServerConfig( + name="my-tools", + command="python", + args=["path/to/server.py"], + env={"DATABASE_URL": os.environ["DATABASE_URL"]} +) +``` + +## Docker MCP Servers + +Run MCP servers in containers: + +```python +docker_mcp = MCPServerConfig( + name="secure-tools", + command="docker", + args=[ + "run", "--rm", "-i", + "-e", f"API_KEY={os.environ['API_KEY']}", + "my-mcp-server:latest" + ] +) +``` + +## Event Handling + +Monitor MCP tool usage: + +```python +from copilot.types import SessionEventType + +def create_mcp_handler(): + """Track MCP tool execution.""" + def handler(event): + if event.type == SessionEventType.TOOL_EXECUTION_START: + tool_name = event.data.tool_name + if tool_name.startswith("github."): + print(f"πŸ™ GitHub: {tool_name}") + elif tool_name.startswith("filesystem."): + print(f"πŸ“ Filesystem: {tool_name}") + + elif event.type == SessionEventType.TOOL_EXECUTION_COMPLETE: + print(f"βœ… Completed") + + elif event.type == SessionEventType.SESSION_ERROR: + print(f"❌ Error: {event.data.message}") + + return handler + +session.on(create_mcp_handler()) +``` + +## Error Handling + +Handle MCP server issues: + +```python +async def safe_mcp_session(client, mcp_servers): + """Create session with MCP error handling.""" + try: + session = await client.create_session({ + "mcp_servers": mcp_servers + }) + return session + + except FileNotFoundError as e: + print(f"MCP server not found: {e}") + print("Try: npm install -g @modelcontextprotocol/server-github") + raise + + except PermissionError as e: + print(f"Permission denied: {e}") + print("Check environment variables and file permissions") + raise + + except TimeoutError: + print("MCP server timed out during startup") + raise +``` + +## Available MCP Servers + +| Server | Package | Description | +|--------|---------|-------------| +| GitHub | `@modelcontextprotocol/server-github` | GitHub API | +| Filesystem | `@modelcontextprotocol/server-filesystem` | Local files | +| Slack | `@modelcontextprotocol/server-slack` | Slack API | +| PostgreSQL | `@modelcontextprotocol/server-postgres` | Database | +| Brave Search | `@modelcontextprotocol/server-brave-search` | Web search | + +## Best Practices + +1. **Secure credentials**: Use environment variables for tokens +2. **Limit access**: Use tool filtering for security +3. **Handle errors**: MCP servers can fail independently +4. **Monitor usage**: Log tool calls for debugging +5. **Test locally**: Verify MCP servers work before deploying + +## Complete Example + +```bash +# Set up environment +export GITHUB_TOKEN=ghp_... + +python recipe/mcp_servers.py +``` + +Demonstrates: +- GitHub MCP server +- Filesystem MCP server +- Tool filtering +- Multiple servers + +## Next Steps + +- [Custom Tools](custom-tools.md): Combine MCP with custom tools +- [Custom Agents](custom-agents.md): Use MCP tools in agents +- [Error Handling](error-handling.md): Handle MCP errors diff --git a/cookbook/python/multiple-sessions.md b/cookbook/python/multiple-sessions.md index 6e0cff41..db31c999 100644 --- a/cookbook/python/multiple-sessions.md +++ b/cookbook/python/multiple-sessions.md @@ -1,78 +1,284 @@ # Working with Multiple Sessions -Manage multiple independent conversations simultaneously. +Manage multiple independent conversations simultaneously for multi-user or multi-task applications. -> **Runnable example:** [recipe/multiple_sessions.py](recipe/multiple_sessions.py) +> **Skill Level:** Beginner to Intermediate +> +> **Runnable Example:** [recipe/multiple_sessions.py](recipe/multiple_sessions.py) > > ```bash > cd recipe && pip install -r requirements.txt > python multiple_sessions.py > ``` -## Example scenario +## Overview + +This recipe covers managing multiple conversation sessions: -You need to run multiple conversations in parallel, each with its own context and history. +- Creating independent sessions with isolated contexts +- Using different models for different sessions +- Parallel execution of multiple requests +- Custom session IDs for tracking +- Session pool pattern for scalable applications -## Python +## Quick Start ```python +import asyncio from copilot import CopilotClient -client = CopilotClient() -client.start() - -# Create multiple independent sessions -session1 = client.create_session(model="gpt-5") -session2 = client.create_session(model="gpt-5") -session3 = client.create_session(model="claude-sonnet-4.5") - -# Each session maintains its own conversation history -session1.send(prompt="You are helping with a Python project") -session2.send(prompt="You are helping with a TypeScript project") -session3.send(prompt="You are helping with a Go project") - -# Follow-up messages stay in their respective contexts -session1.send(prompt="How do I create a virtual environment?") -session2.send(prompt="How do I set up tsconfig?") -session3.send(prompt="How do I initialize a module?") - -# Clean up all sessions -session1.destroy() -session2.destroy() -session3.destroy() -client.stop() +async def main(): + client = CopilotClient() + await client.start() + + # Create multiple independent sessions + session1 = await client.create_session() + session2 = await client.create_session() + session3 = await client.create_session({"model": "claude-sonnet-4"}) + + # Each session has its own context + await session1.send_and_wait({"prompt": "You're helping with Python"}) + await session2.send_and_wait({"prompt": "You're helping with TypeScript"}) + await session3.send_and_wait({"prompt": "You're helping with Go"}) + + # Context-aware follow-ups + await session1.send_and_wait({"prompt": "How do I create a virtual environment?"}) + await session2.send_and_wait({"prompt": "How do I set up tsconfig?"}) + await session3.send_and_wait({"prompt": "How do I initialize a module?"}) + + # Clean up + await session1.destroy() + await session2.destroy() + await session3.destroy() + await client.stop() + +asyncio.run(main()) ``` -## Custom session IDs +## Custom Session IDs -Use custom IDs for easier tracking: +Use meaningful IDs for easier tracking and management: ```python -session = client.create_session( - session_id="user-123-chat", - model="gpt-5" -) +# Create sessions with custom IDs +user_session = await client.create_session({ + "session_id": "user-123-chat" +}) + +support_session = await client.create_session({ + "session_id": "support-ticket-456" +}) + +print(user_session.session_id) # "user-123-chat" +``` + +## Parallel Execution + +Execute requests across multiple sessions concurrently: + +```python +import asyncio +from copilot import CopilotClient +from copilot.types import SessionEventType + +async def parallel_requests(): + client = CopilotClient() + await client.start() + + # Create sessions + topics = ["Python", "JavaScript", "Rust"] + sessions = [await client.create_session() for _ in topics] + + # Collect responses + results = {} + + def make_handler(topic, results_dict): + def handler(event): + if event.type == SessionEventType.ASSISTANT_MESSAGE: + results_dict[topic] = event.data.content + return handler + + # Set up handlers + for session, topic in zip(sessions, topics): + session.on(make_handler(topic, results)) + + # Execute all requests in parallel + await asyncio.gather(*[ + session.send_and_wait({"prompt": f"What is {topic}? One sentence."}) + for session, topic in zip(sessions, topics) + ]) + + # All results available now + for topic, response in results.items(): + print(f"{topic}: {response}") -print(session.session_id) # "user-123-chat" + # Cleanup + for session in sessions: + await session.destroy() + await client.stop() ``` -## Listing sessions +## Session Listing + +View all active sessions: ```python -sessions = client.list_sessions() +# List all sessions +sessions = await client.list_sessions() + for session_info in sessions: print(f"Session: {session_info['sessionId']}") + print(f" Modified: {session_info['modifiedTime']}") + if session_info.get('summary'): + print(f" Summary: {session_info['summary']}") +``` + +## Session Pool Pattern + +For high-throughput applications, use a session pool: + +```python +class SessionPool: + """Reusable pool of sessions for concurrent requests.""" + + def __init__(self, client, size=5): + self.client = client + self.size = size + self._available = asyncio.Queue() + self._all_sessions = [] + + async def initialize(self): + """Create the pool of sessions.""" + for i in range(self.size): + session = await self.client.create_session({ + "session_id": f"pool-{i}" + }) + self._all_sessions.append(session) + await self._available.put(session) + + async def acquire(self, timeout=30.0): + """Get a session from the pool.""" + return await asyncio.wait_for( + self._available.get(), + timeout=timeout + ) + + async def release(self, session): + """Return a session to the pool.""" + await self._available.put(session) + + async def close(self): + """Destroy all sessions.""" + for session in self._all_sessions: + await session.destroy() + + +# Usage +pool = SessionPool(client, size=5) +await pool.initialize() + +# Process requests concurrently +session = await pool.acquire() +try: + await session.send_and_wait({"prompt": "Question"}) +finally: + await pool.release(session) + +await pool.close() +``` + +## Use Cases + +### Multi-User Chat Application + +```python +# One session per user +async def get_or_create_session(client, user_id): + session_id = f"user-{user_id}" + try: + return await client.resume_session(session_id) + except RuntimeError: + return await client.create_session({"session_id": session_id}) +``` + +### Multi-Task Workflows + +```python +# Different sessions for different tasks +planning = await client.create_session({"session_id": "task-planning"}) +coding = await client.create_session({"session_id": "task-coding"}) +review = await client.create_session({"session_id": "task-review"}) ``` -## Deleting sessions +### A/B Testing Models ```python -# Delete a specific session -client.delete_session("user-123-chat") +# Compare responses from different models +gpt_session = await client.create_session({"model": "gpt-5"}) +claude_session = await client.create_session({"model": "claude-sonnet-4"}) + +# Same prompt, different models +prompt = {"prompt": "Explain quantum computing in one paragraph."} +await asyncio.gather( + gpt_session.send_and_wait(prompt), + claude_session.send_and_wait(prompt) +) +``` + +## Session Lifecycle + ``` + create_session() + β”‚ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Active Session β”‚ + β”‚ - send() / send_and_wait() β”‚ + β”‚ - on() for events β”‚ + β”‚ - get_messages() for history β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β–Ό β–Ό + destroy() resume_session() + β”‚ β”‚ + β–Ό β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ + β”‚ Destroyed β”‚ β—„β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚(persisted) β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + delete_session() + β”‚ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Deleted β”‚ + β”‚(permanent) β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## Best Practices + +1. **Use meaningful session IDs**: Include user, task, or date identifiers +2. **Clean up sessions**: Always call `destroy()` when done +3. **Limit concurrent sessions**: Use session pools for high volume +4. **Handle session not found**: Wrap `resume_session()` in try-except + +## Complete Example + +```bash +python recipe/multiple_sessions.py +``` + +Demonstrates: +- Basic multiple sessions +- Parallel execution +- Custom session IDs +- Session pool pattern -## Use cases +## Next Steps -- **Multi-user applications**: One session per user -- **Multi-task workflows**: Separate sessions for different tasks -- **A/B testing**: Compare responses from different models +- [Persisting Sessions](persisting-sessions.md): Save and resume across restarts +- [Error Handling](error-handling.md): Handle session errors gracefully +- [Streaming Responses](streaming-responses.md): Real-time response handling diff --git a/cookbook/python/persisting-sessions.md b/cookbook/python/persisting-sessions.md index e0dfb797..4f117957 100644 --- a/cookbook/python/persisting-sessions.md +++ b/cookbook/python/persisting-sessions.md @@ -1,83 +1,272 @@ -# Session Persistence and Resumption +# Persisting Sessions -Save and restore conversation sessions across application restarts. +Save and resume conversations across application restarts. -## Example scenario - -You want users to be able to continue a conversation even after closing and reopening your application. - -> **Runnable example:** [recipe/persisting_sessions.py](recipe/persisting_sessions.py) +> **Skill Level:** Beginner to Intermediate +> +> **Runnable Example:** [recipe/persisting_sessions.py](recipe/persisting_sessions.py) > > ```bash > cd recipe && pip install -r requirements.txt > python persisting_sessions.py > ``` -### Creating a session with a custom ID +## Overview + +This recipe demonstrates session persistence patterns: + +- Basic save and resume functionality +- Custom session IDs for organization +- Infinite sessions that never expire +- Conversation bookmarks and history export +- Session management and cleanup + +## Quick Start ```python +import asyncio from copilot import CopilotClient -client = CopilotClient() -client.start() +async def main(): + client = CopilotClient() + await client.start() + + # Create a session with a memorable ID + session = await client.create_session({ + "session_id": "project-discussion-2024" + }) + + # Have a conversation + await session.send_and_wait({"prompt": "Let's plan a web app architecture"}) + await session.send_and_wait({"prompt": "What database should we use?"}) -# Create session with a memorable ID -session = client.create_session( - session_id="user-123-conversation", - model="gpt-5", -) + # Destroy (but preserve for resuming) + await session.destroy() -session.send(prompt="Let's discuss TypeScript generics") + # Later... resume the session + resumed = await client.resume_session("project-discussion-2024") + await resumed.send_and_wait({"prompt": "What were we discussing?"}) + # Session remembers the full conversation context! -# Session ID is preserved -print(session.session_id) # "user-123-conversation" + await resumed.destroy() + await client.stop() -# Destroy session but keep data on disk -session.destroy() -client.stop() +asyncio.run(main()) ``` -### Resuming a session +## Session Lifecycle + +``` +create_session() + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Active Session β”‚ +β”‚ - send() / send_and_wait() β”‚ +β”‚ - Full context maintained β”‚ +β”‚ - on() for event handling β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό destroy() +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Resumable Session β”‚ +β”‚ - Persisted to storage β”‚ +β”‚ - Can be listed via list_sessions β”‚ +β”‚ - Context preserved β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”œβ”€β”€β–Ά resume_session() ──▢ Active Session + β”‚ + β–Ό delete_session() +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Permanently Deleted β”‚ +β”‚ - Cannot be recovered β”‚ +β”‚ - All history lost β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## Custom Session IDs + +Use meaningful IDs for easy organization: ```python -client = CopilotClient() -client.start() +# User-based sessions +user_session = await client.create_session({ + "session_id": f"user-{user_id}-main" +}) + +# Project-based sessions +project_session = await client.create_session({ + "session_id": f"project-{project_name}-2024" +}) + +# Task-based sessions +task_session = await client.create_session({ + "session_id": f"task-{task_id}-review" +}) +``` -# Resume the previous session -session = client.resume_session("user-123-conversation") +## Infinite Sessions -# Previous context is restored -session.send(prompt="What were we discussing?") +Sessions that never expire (useful for long-running applications): -session.destroy() -client.stop() +```python +infinite_session = await client.create_session({ + "session_id": "persistent-assistant", + "infinite_sessions": True # Never expires +}) ``` -### Listing available sessions +## Conversation History + +Access and export conversation history: ```python -sessions = client.list_sessions() -for s in sessions: - print("Session:", s["sessionId"]) +# Get all messages +messages = session.get_messages() + +for msg in messages: + print(f"[{msg.role}] {msg.content}") + +# Export to JSON +import json + +history = [ + {"role": msg.role, "content": msg.content, "id": msg.id} + for msg in messages +] + +with open("conversation.json", "w") as f: + json.dump(history, f, indent=2) ``` -### Deleting a session permanently +## Session Management + +List and manage all sessions: ```python -# Remove session and all its data from disk -client.delete_session("user-123-conversation") +# List all available sessions +sessions = await client.list_sessions() + +for session_info in sessions: + sid = session_info.get('sessionId', 'unknown') + modified = session_info.get('modifiedTime', 'N/A') + summary = session_info.get('summary', 'No summary') + + print(f"Session: {sid}") + print(f" Modified: {modified}") + print(f" Summary: {summary}") ``` -### Getting session history +## Safe Resume with Fallback + +Handle cases where session may not exist: ```python -messages = session.get_messages() -for msg in messages: - print(f"[{msg['type']}] {msg['data']}") +async def get_or_create_session(client, session_id, config=None): + """Resume existing session or create new one.""" + try: + return await client.resume_session(session_id) + except RuntimeError: + return await client.create_session({ + "session_id": session_id, + **(config or {}) + }) + +# Usage +session = await get_or_create_session(client, "my-session") +``` + +## Conversation Bookmarks + +Mark important points in a conversation: + +```python +class ConversationBookmarks: + """Track important points in a conversation.""" + + def __init__(self): + self.bookmarks = {} + + def mark(self, session, name, description=""): + """Mark current position in conversation.""" + messages = session.get_messages() + self.bookmarks[name] = { + "message_index": len(messages) - 1, + "message_id": messages[-1].id if messages else None, + "description": description + } + + def get_context_since(self, session, bookmark_name): + """Get all messages since a bookmark.""" + if bookmark_name not in self.bookmarks: + return [] + + bookmark = self.bookmarks[bookmark_name] + messages = session.get_messages() + return messages[bookmark["message_index"] + 1:] + + +# Usage +bookmarks = ConversationBookmarks() + +await session.send_and_wait({"prompt": "Let's start the design"}) +bookmarks.mark(session, "design_start", "Beginning of design discussion") + +await session.send_and_wait({"prompt": "Now let's discuss implementation"}) +bookmarks.mark(session, "implementation_start") + +# Later, get context since a bookmark +design_discussion = bookmarks.get_context_since(session, "design_start") ``` -## Best practices +## Permanent Deletion + +When you're completely done with a session: + +```python +# Permanently delete - cannot be recovered +await client.delete_session("old-session-id") + +# Cleanup old sessions +sessions = await client.list_sessions() +for session_info in sessions: + if is_old(session_info): + await client.delete_session(session_info['sessionId']) +``` + +## Use Cases + +| Use Case | Pattern | +|----------|---------| +| User preferences | One persistent session per user | +| Project discussions | Sessions named by project | +| Audit trails | Export history before deletion | +| Long-running assistants | Infinite sessions | +| Multi-day workflows | Resume with full context | + +## Best Practices + +1. **Use meaningful session IDs**: Include user, project, or date identifiers +2. **Export before deleting**: Save important conversations to files +3. **Clean up old sessions**: Periodically remove unused sessions +4. **Handle resume failures**: Always wrap `resume_session()` in try-except +5. **Use infinite sessions carefully**: Only for truly persistent assistants + +## Complete Example + +```bash +python recipe/persisting_sessions.py +``` + +Demonstrates: +- Basic persistence and resume +- Session management +- Infinite sessions +- Conversation bookmarks and export + +## Next Steps -1. **Use meaningful session IDs**: Include user ID or context in the session ID -2. **Handle missing sessions**: Check if a session exists before resuming -3. **Clean up old sessions**: Periodically delete sessions that are no longer needed +- [Multiple Sessions](multiple-sessions.md): Manage concurrent sessions +- [Error Handling](error-handling.md): Handle persistence errors +- [Custom Tools](custom-tools.md): Add tools to persistent sessions diff --git a/cookbook/python/pr-visualization.md b/cookbook/python/pr-visualization.md index af2ce20c..efd25cd9 100644 --- a/cookbook/python/pr-visualization.md +++ b/cookbook/python/pr-visualization.md @@ -1,8 +1,10 @@ -# Generating PR Age Charts +# PR Visualization and Analytics -Build an interactive CLI tool that visualizes pull request age distribution for a GitHub repository using Copilot's built-in capabilities. +Build interactive CLI tools for GitHub PR analysis and visualization. -> **Runnable example:** [recipe/pr_visualization.py](recipe/pr_visualization.py) +> **Skill Level:** Intermediate to Advanced +> +> **Runnable Example:** [recipe/pr_visualization.py](recipe/pr_visualization.py) > > ```bash > cd recipe && pip install -r requirements.txt @@ -13,9 +15,15 @@ Build an interactive CLI tool that visualizes pull request age distribution for > python pr_visualization.py --repo github/copilot-sdk > ``` -## Example scenario +## Overview + +This recipe demonstrates PR analytics capabilities: -You want to understand how long PRs have been open in a repository. This tool detects the current Git repo or accepts a repo as input, then lets Copilot fetch PR data via the GitHub MCP Server and generate a chart image. +- Auto-detecting GitHub repositories +- PR age analysis and charting +- Author and review status analysis +- Interactive follow-up queries +- AI-powered data visualization ## Prerequisites @@ -23,196 +31,253 @@ You want to understand how long PRs have been open in a repository. This tool de pip install copilot-sdk ``` -## Usage +## Quick Start -```bash -# Auto-detect from current git repo -python pr_breakdown.py +```python +import asyncio +from copilot import CopilotClient +from copilot.types import SessionEventType -# Specify a repo explicitly -python pr_breakdown.py --repo github/copilot-sdk -``` +async def main(): + client = CopilotClient() + await client.start() -## Full example: pr_breakdown.py + session = await client.create_session({ + "system_message": { + "content": "You are analyzing PRs for github/copilot-sdk" + } + }) -```python -#!/usr/bin/env python3 + def handle_event(event): + if event.type == SessionEventType.ASSISTANT_MESSAGE: + print(f"πŸ€– {event.data.content}") -import subprocess -import sys -import os -from copilot import CopilotClient + session.on(handle_event) -# ============================================================================ -# Git & GitHub Detection -# ============================================================================ + await session.send_and_wait({ + "prompt": """ +Fetch open pull requests for the repo. +Calculate the age of each PR. +Generate a bar chart showing PR age distribution. +Save as 'pr-chart.png'. +""" + }, timeout=300.0) -def is_git_repo(): - try: - subprocess.run( - ["git", "rev-parse", "--git-dir"], - check=True, - capture_output=True - ) - return True - except (subprocess.CalledProcessError, FileNotFoundError): - return False + await session.destroy() + await client.stop() + +asyncio.run(main()) +``` + +## Repository Detection + +Auto-detect GitHub repo from git remote: + +```python +import subprocess +import re def get_github_remote(): + """Detect GitHub repository from git remote.""" try: result = subprocess.run( ["git", "remote", "get-url", "origin"], - check=True, - capture_output=True, - text=True + capture_output=True, text=True, check=True ) - remote_url = result.stdout.strip() - - # Handle SSH: git@github.com:owner/repo.git - import re - ssh_match = re.search(r"git@github\.com:(.+/.+?)(?:\.git)?$", remote_url) - if ssh_match: - return ssh_match.group(1) - - # Handle HTTPS: https://github.com/owner/repo.git - https_match = re.search(r"https://github\.com/(.+/.+?)(?:\.git)?$", remote_url) - if https_match: - return https_match.group(1) - - return None - except (subprocess.CalledProcessError, FileNotFoundError): - return None - -def parse_args(): - args = sys.argv[1:] - if "--repo" in args: - idx = args.index("--repo") - if idx + 1 < len(args): - return {"repo": args[idx + 1]} - return {} - -def prompt_for_repo(): - return input("Enter GitHub repo (owner/repo): ").strip() - -# ============================================================================ -# Main Application -# ============================================================================ - -def main(): - print("πŸ” PR Age Chart Generator\n") - - # Determine the repository - args = parse_args() - repo = None - - if "repo" in args: - repo = args["repo"] - print(f"πŸ“¦ Using specified repo: {repo}") - elif is_git_repo(): - detected = get_github_remote() - if detected: - repo = detected - print(f"πŸ“¦ Detected GitHub repo: {repo}") - else: - print("⚠️ Git repo found but no GitHub remote detected.") - repo = prompt_for_repo() - else: - print("πŸ“ Not in a git repository.") - repo = prompt_for_repo() - - if not repo or "/" not in repo: - print("❌ Invalid repo format. Expected: owner/repo") - sys.exit(1) - - owner, repo_name = repo.split("/", 1) - - # Create Copilot client - no custom tools needed! - client = CopilotClient(log_level="error") - client.start() - - session = client.create_session( - model="gpt-5", - system_message={ - "content": f""" - -You are analyzing pull requests for the GitHub repository: {owner}/{repo_name} -The current working directory is: {os.getcwd()} - + remote = result.stdout.strip() - -- Use the GitHub MCP Server tools to fetch PR data -- Use your file and code execution tools to generate charts -- Save any generated images to the current working directory -- Be concise in your responses - + # SSH format: git@github.com:owner/repo.git + ssh = re.search(r"git@github\.com:(.+/.+?)(?:\.git)?$", remote) + if ssh: + return ssh.group(1) + + # HTTPS format: https://github.com/owner/repo.git + https = re.search(r"https://github\.com/(.+/.+?)(?:\.git)?$", remote) + if https: + return https.group(1) + + except Exception: + pass + return None + +# Usage +repo = get_github_remote() or input("Enter repo (owner/repo): ") +``` + +## Analysis Types + +### PR Age Analysis + +```python +await session.send_and_wait({ + "prompt": """ +Fetch open PRs and analyze their age: +1. Calculate days open for each PR +2. Group into buckets: <1 day, 1-3 days, 3-7 days, 7+ days +3. Generate a bar chart +4. List the 5 oldest PRs """ - } - ) +}) +``` - # Set up event handling - def handle_event(event): - if event["type"] == "assistant.message": - print(f"\nπŸ€– {event['data']['content']}\n") - elif event["type"] == "tool.execution_start": - print(f" βš™οΈ {event['data']['toolName']}") +### Author Analysis - session.on(handle_event) +```python +await session.send_and_wait({ + "prompt": """ +Analyze PRs by author: +1. Count PRs per author +2. Show average review time per author +3. Generate a pie chart of PR distribution +4. Identify most active contributors +""" +}) +``` + +### Review Status Analysis - # Initial prompt - let Copilot figure out the details - print("\nπŸ“Š Starting analysis...\n") +```python +await session.send_and_wait({ + "prompt": """ +Analyze review status of open PRs: +1. Count: needs review, approved, changes requested +2. Calculate average time to first review +3. Identify PRs without any reviews +4. Generate a status breakdown chart +""" +}) +``` - session.send(prompt=f""" - Fetch the open pull requests for {owner}/{repo_name} from the last week. - Calculate the age of each PR in days. - Then generate a bar chart image showing the distribution of PR ages - (group them into sensible buckets like <1 day, 1-3 days, etc.). - Save the chart as "pr-age-chart.png" in the current directory. - Finally, summarize the PR health - average age, oldest PR, and how many might be considered stale. - """) +## Interactive CLI - session.wait_for_idle() +Build an interactive analysis tool: + +```python +async def interactive_analysis(session, repo): + """Interactive PR analysis loop.""" - # Interactive loop - print("\nπŸ’‘ Ask follow-up questions or type \"exit\" to quit.\n") + # Initial analysis + await session.send_and_wait({ + "prompt": f"Analyze open PRs for {repo}: count, average age, health summary" + }) + + print("\nπŸ’‘ Ask follow-up questions or type 'exit' to quit") print("Examples:") - print(" - \"Expand to the last month\"") - print(" - \"Show me the 5 oldest PRs\"") - print(" - \"Generate a pie chart instead\"") - print(" - \"Group by author instead of age\"") - print() + print(" - 'Show the 5 oldest PRs'") + print(" - 'Group by author'") + print(" - 'Generate a pie chart'") + print(" - 'Check for stale PRs'") while True: - user_input = input("You: ").strip() - - if user_input.lower() in ["exit", "quit"]: - print("πŸ‘‹ Goodbye!") + try: + query = input("\nYou: ").strip() + if query.lower() in ['exit', 'quit']: + break + if query: + await session.send_and_wait({"prompt": query}, timeout=300.0) + except (EOFError, KeyboardInterrupt): break +``` - if user_input: - session.send(prompt=user_input) - session.wait_for_idle() +## Event Handling - client.stop() +Track analysis progress: -if __name__ == "__main__": - main() +```python +def create_event_handler(): + """Create event handler for analysis visibility.""" + def handler(event): + if event.type == SessionEventType.ASSISTANT_MESSAGE: + print(f"\nπŸ€– {event.data.content}") + elif event.type == SessionEventType.TOOL_EXECUTION_START: + print(f" βš™οΈ Running: {event.data.tool_name}") + elif event.type == SessionEventType.TOOL_EXECUTION_COMPLETE: + print(f" βœ“ Completed") + elif event.type == SessionEventType.SESSION_ERROR: + print(f" ❌ Error: {event.data.message}") + + return handler + +session.on(create_event_handler()) +``` + +## Chart Generation + +Copilot can generate various charts: + +```python +# Bar chart +await session.send_and_wait({ + "prompt": "Generate a bar chart of PR ages, save as pr-ages.png" +}) + +# Pie chart +await session.send_and_wait({ + "prompt": "Generate a pie chart of PRs by status, save as pr-status.png" +}) + +# Timeline +await session.send_and_wait({ + "prompt": "Generate a timeline of PR creation dates, save as pr-timeline.png" +}) ``` -## How it works +## Why Use Copilot for This? + +| Aspect | Custom Code | Copilot Approach | +|--------|-------------|------------------| +| Complexity | High (GitHub API, matplotlib) | **Minimal** | +| Maintenance | You maintain | **Copilot maintains** | +| Flexibility | Fixed logic | **AI-determined** | +| Chart types | What you coded | **Any type** | +| Grouping | Hardcoded | **Intelligent** | + +## System Message Configuration + +Set up context for better analysis: + +```python +session = await client.create_session({ + "system_message": { + "content": f""" + +Repository: {owner}/{repo_name} +Working directory: {os.getcwd()} + + + +- Use GitHub MCP Server for PR data +- Use file tools to save charts +- Be concise in responses +- Focus on actionable insights + +""" + } +}) +``` + +## Best Practices + +1. **Set appropriate timeouts**: GitHub API + chart generation can take time +2. **Use system messages**: Provide clear context about the repository +3. **Handle rate limits**: GitHub API has rate limits +4. **Save charts locally**: Specify save paths in the current directory +5. **Interactive follow-up**: Allow users to refine analysis + +## Complete Example + +```bash +python recipe/pr_visualization.py +``` -1. **Repository detection**: Checks `--repo` flag β†’ git remote β†’ prompts user -2. **No custom tools**: Relies entirely on Copilot CLI's built-in capabilities: - - **GitHub MCP Server** - Fetches PR data from GitHub - - **File tools** - Saves generated chart images - - **Code execution** - Generates charts using Python/matplotlib or other methods -3. **Interactive session**: After initial analysis, user can ask for adjustments +Demonstrates: +- Repository auto-detection +- PR age analysis and charting +- Interactive follow-up queries +- Multiple analysis types -## Why this approach? +## Next Steps -| Aspect | Custom Tools | Built-in Copilot | -| --------------- | ----------------- | --------------------------------- | -| Code complexity | High | **Minimal** | -| Maintenance | You maintain | **Copilot maintains** | -| Flexibility | Fixed logic | **AI decides best approach** | -| Chart types | What you coded | **Any type Copilot can generate** | -| Data grouping | Hardcoded buckets | **Intelligent grouping** | +- [Custom Tools](custom-tools.md): Create specialized PR analysis tools +- [MCP Servers](mcp-servers.md): Configure GitHub MCP integration +- [Streaming Responses](streaming-responses.md): Real-time analysis updates diff --git a/cookbook/python/recipe/README.md b/cookbook/python/recipe/README.md index aab80173..eb1d5aad 100644 --- a/cookbook/python/recipe/README.md +++ b/cookbook/python/recipe/README.md @@ -4,7 +4,7 @@ This folder contains standalone, executable Python examples for each cookbook re ## Prerequisites -- Python 3.8 or later +- Python 3.9 or later - Install dependencies (this installs the local SDK in editable mode): ```bash @@ -23,13 +23,18 @@ python .py ### Available Recipes -| Recipe | Command | Description | -| -------------------- | -------------------------------- | ------------------------------------------ | -| Error Handling | `python error_handling.py` | Demonstrates error handling patterns | -| Multiple Sessions | `python multiple_sessions.py` | Manages multiple independent conversations | -| Managing Local Files | `python managing_local_files.py` | Organizes files using AI grouping | -| PR Visualization | `python pr_visualization.py` | Generates PR age charts | -| Persisting Sessions | `python persisting_sessions.py` | Save and resume sessions across restarts | +| Recipe | Command | Description | +| ------ | ------- | ----------- | +| Custom Agents | `python custom_agents.py` | Specialized AI agents with custom prompts | +| Custom Providers | `python custom_providers.py` | BYOK: OpenAI, Azure, Anthropic providers | +| Custom Tools | `python custom_tools.py` | Define custom tools for Copilot | +| Error Handling | `python error_handling.py` | Async error handling patterns | +| Managing Local Files | `python managing_local_files.py` | AI-powered file organization | +| MCP Servers | `python mcp_servers.py` | Model Context Protocol integration | +| Multiple Sessions | `python multiple_sessions.py` | Manage independent conversations | +| Persisting Sessions | `python persisting_sessions.py` | Save and resume sessions | +| PR Visualization | `python pr_visualization.py` | Generate PR age charts | +| Streaming Responses | `python streaming_responses.py` | Real-time streaming output | ### Examples with Arguments @@ -39,16 +44,64 @@ python .py python pr_visualization.py --repo github/copilot-sdk ``` -**Managing Local Files (edit the file to change target folder):** +**Managing Local Files (quick mode):** ```bash -# Edit the target_folder variable in managing_local_files.py first -python managing_local_files.py +python managing_local_files.py --quick ``` +## About the SDK API + +The Copilot SDK is fully asynchronous. All examples use `asyncio.run()` to run the async main function: + +```python +import asyncio +from copilot import CopilotClient +from copilot.types import SessionEventType + +async def main(): + client = CopilotClient() + await client.start() + + session = await client.create_session() + + def handler(event): + if event.type == SessionEventType.ASSISTANT_MESSAGE: + print(event.data.content) + + session.on(handler) + await session.send_and_wait({"prompt": "Hello!"}) + + await session.destroy() + await client.stop() + +if __name__ == "__main__": + asyncio.run(main()) +``` + +### Key API Patterns + +- **Async methods**: `start()`, `stop()`, `create_session()`, `send()`, `destroy()` all require `await` +- **Configuration dicts**: Pass options as dictionaries, e.g., `{"prompt": "Hello"}` +- **Event handling**: Use `SessionEventType` enum for type-safe event comparisons +- **Event objects**: Events have `.type` and `.data` attributes (not dict access) +- **send_and_wait()**: Convenience method that sends and waits for completion + +### SessionEventType Values + +| Event Type | Description | +| ---------- | ----------- | +| `SessionEventType.ASSISTANT_MESSAGE` | Complete assistant message | +| `SessionEventType.ASSISTANT_MESSAGE_DELTA` | Streaming message chunk | +| `SessionEventType.TOOL_EXECUTION_START` | Tool execution started | +| `SessionEventType.TOOL_EXECUTION_COMPLETE` | Tool execution completed | +| `SessionEventType.SESSION_IDLE` | Session is idle | +| `SessionEventType.SESSION_ERROR` | Session error occurred | +| `SessionEventType.SUBAGENT_SELECTED` | Custom agent was selected | + ## Local SDK Development -The `requirements.txt` installs the local Copilot SDK using `-e ../..` (editable install). This means: +The `requirements.txt` installs the local Copilot SDK using `-e ../../../python` (editable install). This means: - Changes to the SDK source are immediately available - No need to publish or install from PyPI @@ -64,7 +117,8 @@ These examples follow Python conventions: - Shebang line for direct execution - Proper exception handling - Type hints where appropriate -- Standard library usage +- Module docstrings for documentation +- Async/await patterns ## Virtual Environment (Recommended) @@ -87,6 +141,7 @@ pip install -r requirements.txt ## Learning Resources - [Python Documentation](https://docs.python.org/3/) +- [Python asyncio](https://docs.python.org/3/library/asyncio.html) - [PEP 8 Style Guide](https://pep8.org/) -- [GitHub Copilot SDK for Python](../../README.md) +- [GitHub Copilot SDK for Python](../../../python/README.md) - [Parent Cookbook](../README.md) diff --git a/cookbook/python/recipe/custom_agents.py b/cookbook/python/recipe/custom_agents.py new file mode 100644 index 00000000..2a0eff64 --- /dev/null +++ b/cookbook/python/recipe/custom_agents.py @@ -0,0 +1,508 @@ +#!/usr/bin/env python3 +""" +Custom Agents - Creating specialized AI assistants with the Copilot SDK. +Run: python custom_agents.py +""" + +import asyncio + +from pydantic import BaseModel, Field + +from copilot import CopilotClient +from copilot.tools import define_tool +from copilot.types import CustomAgentConfig, SessionEventType + + +# ============================================================================= +# Custom Agent Configurations +# ============================================================================= + + +def create_code_reviewer_agent(): + """Create a code review specialist agent.""" + return CustomAgentConfig( + name="code-reviewer", + display_name="Code Review Expert", + description="Specializes in code review, finding bugs, and suggesting improvements", + prompt=""" +You are an expert code reviewer with decades of experience in software development. + +Your responsibilities: +1. Review code for bugs, security issues, and performance problems +2. Suggest improvements for readability and maintainability +3. Identify potential edge cases and error handling gaps +4. Recommend best practices and design patterns +5. Be constructive and educational in your feedback + +When reviewing code: +- Start with a high-level summary +- List specific issues with line references +- Categorize issues by severity (critical, major, minor, suggestion) +- Provide example fixes when helpful + +Your tone should be professional, constructive, and encouraging. +""", + tools=None, # Use default tools + infer=True, # Available for model inference + ) + + +def create_sql_expert_agent(): + """Create a SQL database expert agent.""" + return CustomAgentConfig( + name="sql-expert", + display_name="SQL Database Expert", + description="Specializes in SQL queries, database design, and optimization", + prompt=""" +You are a senior database engineer and SQL expert. + +Your expertise includes: +1. Writing efficient SQL queries for various databases +2. Database schema design and normalization +3. Query optimization and performance tuning +4. Index strategies and execution plan analysis +5. Database migration strategies +6. Data modeling best practices + +When helping with SQL: +- Ask clarifying questions about the database system (PostgreSQL, MySQL, SQLite, etc.) +- Explain query logic step by step +- Warn about potential performance issues +- Suggest indexes when appropriate +- Consider edge cases and NULL handling + +Always format SQL queries properly with clear indentation. +""", + tools=["sql_query", "explain_plan"], # Only allow specific tools + infer=True, + ) + + +def create_documentation_agent(): + """Create a technical documentation specialist agent.""" + return CustomAgentConfig( + name="doc-writer", + display_name="Documentation Writer", + description="Writes clear, comprehensive technical documentation", + prompt=""" +You are a technical writer who creates clear, helpful documentation. + +Your skills include: +1. Writing API documentation with examples +2. Creating user guides and tutorials +3. Writing README files and project documentation +4. Generating code comments and docstrings +5. Creating architecture documentation + +Documentation principles: +- Start with the "why" before the "how" +- Include practical examples for every concept +- Use consistent formatting and structure +- Write for your audience (beginners vs experts) +- Keep it concise but complete + +Output formats you excel at: +- Markdown documentation +- JSDoc/TSDoc/docstrings +- OpenAPI/Swagger specs +- Mermaid diagrams +""", + tools=None, # All tools available + infer=True, + ) + + +def create_security_auditor_agent(): + """Create a security-focused code auditor agent.""" + return CustomAgentConfig( + name="security-auditor", + display_name="Security Auditor", + description="Finds security vulnerabilities and suggests fixes", + prompt=""" +You are a cybersecurity expert specializing in application security. + +Your focus areas: +1. OWASP Top 10 vulnerabilities +2. Authentication and authorization flaws +3. Input validation and injection attacks +4. Cryptographic issues +5. Secrets management +6. Dependency vulnerabilities +7. API security + +When auditing code: +- Prioritize findings by severity (Critical, High, Medium, Low) +- Provide clear reproduction steps +- Reference CVEs and CWEs where applicable +- Suggest specific remediation steps +- Consider both code and configuration issues + +Be thorough but avoid false positives. Explain the actual risk. +""", + tools=["search_cve", "dependency_check"], # Security-focused tools + infer=True, + ) + + +# ============================================================================= +# Custom Tools for Agents +# ============================================================================= + + +class ExplainPlanParams(BaseModel): + """Parameters for SQL explain plan tool.""" + + query: str = Field(description="The SQL query to explain") + + +@define_tool(description="Explain a SQL query execution plan (simulated)") +def explain_plan(params): + """Simulated SQL explain plan tool.""" + return f""" +Execution Plan for: {params.query[:50]}... + +| Operation | Rows | Cost | +|--------------------|-------|--------| +| Seq Scan | 1000 | 100.00 | +| Index Scan | 50 | 5.50 | +| Hash Join | 50 | 10.25 | + +Note: Simulated plan for demonstration. +""" + + +class SQLQueryParams(BaseModel): + """Parameters for SQL query tool.""" + + query: str = Field(description="SQL query to execute") + database: str = Field(default="demo.db", description="Database to query") + + +@define_tool(description="Execute a SQL query (simulated)") +def sql_query(params): + """Simulated SQL query tool.""" + return f""" +Query executed on {params.database}: +{params.query[:100]}... + +Results (simulated): +| id | name | value | +|----|-----------|--------| +| 1 | Example 1 | 100 | +| 2 | Example 2 | 200 | + +2 rows returned. +""" + + +# ============================================================================= +# Event Handler +# ============================================================================= + + +def create_event_handler(): + """Create an event handler for agent demonstrations.""" + + def handler(event): + if event.type == SessionEventType.ASSISTANT_MESSAGE: + print(f"\nπŸ€– {event.data.content}\n") + elif event.type == SessionEventType.TOOL_EXECUTION_START: + print(f" βš™οΈ {event.data.tool_name}") + elif event.type == SessionEventType.SESSION_ERROR: + message = getattr(event.data, "message", str(event.data)) + print(f" ❌ Error: {message}") + + return handler + + +# ============================================================================= +# Demo: Single Agent +# ============================================================================= + + +async def demo_single_agent(): + """Demonstrate using a single custom agent.""" + print("\n=== Single Custom Agent Demo ===\n") + + client = CopilotClient() + + try: + await client.start() + + # Create session with code reviewer agent + session = await client.create_session( + { + "custom_agents": [create_code_reviewer_agent()], + } + ) + + session.on(create_event_handler()) + + print("Using Code Review Agent to review a code snippet...\n") + + await session.send_and_wait( + { + "prompt": """ +@code-reviewer Please review this Python code: + +```python +def get_user(id): + conn = db.connect() + result = conn.execute(f"SELECT * FROM users WHERE id = {id}") + return result.fetchone() +``` +""" + }, + timeout=120.0, + ) + + await session.destroy() + + finally: + await client.stop() + + +# ============================================================================= +# Demo: Multiple Agents +# ============================================================================= + + +async def demo_multiple_agents(): + """Demonstrate multiple custom agents in one session.""" + print("\n=== Multiple Custom Agents Demo ===\n") + + client = CopilotClient() + + try: + await client.start() + + # Create session with multiple agents + session = await client.create_session( + { + "custom_agents": [ + create_code_reviewer_agent(), + create_documentation_agent(), + create_security_auditor_agent(), + ], + } + ) + + session.on(create_event_handler()) + + print("Multiple agents available:") + print(" - @code-reviewer: Code review expert") + print(" - @doc-writer: Documentation specialist") + print(" - @security-auditor: Security expert") + print() + + # Use the documentation agent + print("Asking the documentation agent for help...\n") + + await session.send_and_wait( + { + "prompt": """ +@doc-writer Write a docstring for this function: + +def calculate_discount(price, percentage, min_amount=0): + if price < min_amount: + return price + return price * (1 - percentage / 100) +""" + }, + timeout=120.0, + ) + + await session.destroy() + + finally: + await client.stop() + + +# ============================================================================= +# Demo: Agent with Tools +# ============================================================================= + + +async def demo_agent_with_tools(): + """Demonstrate an agent with access to specific tools.""" + print("\n=== Agent with Custom Tools Demo ===\n") + + client = CopilotClient() + + try: + await client.start() + + # Create session with SQL expert agent and its tools + session = await client.create_session( + { + "custom_agents": [create_sql_expert_agent()], + "tools": [sql_query, explain_plan], + } + ) + + session.on(create_event_handler()) + + print("Using SQL Expert Agent with database tools...\n") + + await session.send_and_wait( + { + "prompt": """ +@sql-expert Help me optimize this query. First show me the execution plan, +then suggest improvements: + +SELECT u.name, COUNT(o.id) as order_count +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE u.created_at > '2024-01-01' +GROUP BY u.id, u.name +ORDER BY order_count DESC +""" + }, + timeout=120.0, + ) + + await session.destroy() + + finally: + await client.stop() + + +# ============================================================================= +# Demo: Agent Inference +# ============================================================================= + + +async def demo_agent_inference(): + """Demonstrate automatic agent selection based on context.""" + print("\n=== Agent Inference Demo ===\n") + + client = CopilotClient() + + try: + await client.start() + + # Create session with multiple inference-enabled agents + session = await client.create_session( + { + "custom_agents": [ + create_code_reviewer_agent(), + create_documentation_agent(), + create_sql_expert_agent(), + ], + "tools": [sql_query, explain_plan], + } + ) + + session.on(create_event_handler()) + + print("Agents available for inference:") + print(" - Code Reviewer") + print(" - Documentation Writer") + print(" - SQL Expert") + print() + + # The model should infer which agent to use + prompts = [ + "Write a README for a Python CLI tool", + "Review this code: `if x = 5: print('hello')`", + "How do I write a JOIN query in PostgreSQL?", + ] + + for prompt in prompts: + print(f"Prompt: {prompt}\n") + await session.send_and_wait({"prompt": prompt}, timeout=60.0) + print("-" * 50) + + await session.destroy() + + finally: + await client.stop() + + +# ============================================================================= +# Dynamic Agent Creation +# ============================================================================= + + +def create_dynamic_agent(name, specialty, instructions): + """Create a custom agent dynamically based on configuration.""" + return CustomAgentConfig( + name=name, + display_name=specialty, + description=f"Specialized assistant for {specialty}", + prompt=f""" +You are a specialized assistant for {specialty}. + +Instructions: +{instructions} + +Always be helpful, accurate, and professional. +""", + tools=None, # Use default tools + infer=True, + ) + + +async def demo_dynamic_agents(): + """Demonstrate creating agents dynamically.""" + print("\n=== Dynamic Agent Creation Demo ===\n") + + client = CopilotClient() + + try: + await client.start() + + # Create agents dynamically based on configuration + agent_configs = [ + ("python-helper", "Python Programming", "Help with Python code, libraries, and best practices."), + ("api-designer", "REST API Design", "Design RESTful APIs following best practices."), + ] + + agents = [ + create_dynamic_agent(name, specialty, instructions) + for name, specialty, instructions in agent_configs + ] + + session = await client.create_session({"custom_agents": agents}) + session.on(create_event_handler()) + + print(f"Created {len(agents)} dynamic agents") + for name, specialty, _ in agent_configs: + print(f" - @{name}: {specialty}") + print() + + # Test one of the dynamic agents + await session.send_and_wait( + {"prompt": "@python-helper What's the difference between a list and a tuple?"}, + timeout=60.0, + ) + + await session.destroy() + + finally: + await client.stop() + + +# ============================================================================= +# Main +# ============================================================================= + + +async def main(): + """Run custom agent demonstrations.""" + print("=" * 60) + print("Custom Agents") + print("=" * 60) + + await demo_single_agent() + await demo_multiple_agents() + await demo_agent_with_tools() + await demo_dynamic_agents() + + print("\n" + "=" * 60) + print("All demos completed!") + print("=" * 60) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/cookbook/python/recipe/custom_providers.py b/cookbook/python/recipe/custom_providers.py new file mode 100644 index 00000000..5c6ca1a7 --- /dev/null +++ b/cookbook/python/recipe/custom_providers.py @@ -0,0 +1,331 @@ +#!/usr/bin/env python3 +""" +Custom Providers (BYOK) - Bring Your Own Key to use custom AI providers. +Run: python custom_providers.py +""" + +import asyncio +import os + +from copilot import CopilotClient, ProviderConfig +from copilot.types import SessionEventType + + +# ============================================================================= +# Provider Configurations +# ============================================================================= + + +def get_openai_provider(): + """Configure OpenAI as the provider. Requires OPENAI_API_KEY.""" + api_key = os.environ.get("OPENAI_API_KEY") + if not api_key: + raise ValueError("OPENAI_API_KEY environment variable not set") + + return ProviderConfig( + type="openai", + base_url="https://api.openai.com/v1", + api_key=api_key, + wire_api="responses", + ) + + +def get_azure_openai_provider(): + """Configure Azure OpenAI. Requires AZURE_OPENAI_API_KEY and AZURE_OPENAI_ENDPOINT.""" + api_key = os.environ.get("AZURE_OPENAI_API_KEY") + endpoint = os.environ.get("AZURE_OPENAI_ENDPOINT") + + if not api_key: + raise ValueError("AZURE_OPENAI_API_KEY environment variable not set") + if not endpoint: + raise ValueError("AZURE_OPENAI_ENDPOINT environment variable not set") + + return ProviderConfig( + type="azure", + base_url=endpoint, + api_key=api_key, + azure={"api_version": "2024-10-21"}, + ) + + +def get_anthropic_provider(): + """Configure Anthropic Claude. Requires ANTHROPIC_API_KEY.""" + api_key = os.environ.get("ANTHROPIC_API_KEY") + if not api_key: + raise ValueError("ANTHROPIC_API_KEY environment variable not set") + + return ProviderConfig( + type="anthropic", + base_url="https://api.anthropic.com", + api_key=api_key, + ) + + +def get_custom_endpoint_provider(): + """Configure a custom OpenAI-compatible endpoint (local LLM, etc.).""" + endpoint = os.environ.get("CUSTOM_ENDPOINT", "http://localhost:8080/v1") + api_key = os.environ.get("CUSTOM_API_KEY", "not-required") + + return ProviderConfig( + type="openai", + base_url=endpoint, + api_key=api_key, + ) + + +def get_bearer_token_provider(): + """Configure a provider using bearer token authentication.""" + token = os.environ.get("BEARER_TOKEN") + endpoint = os.environ.get("API_ENDPOINT") + + if not token: + raise ValueError("BEARER_TOKEN environment variable not set") + if not endpoint: + raise ValueError("API_ENDPOINT environment variable not set") + + return ProviderConfig( + type="openai", + base_url=endpoint, + bearer_token=token, + ) + + +# ============================================================================= +# Demo: Using Providers +# ============================================================================= + + +async def demo_with_provider(provider_name, provider_config): + """Test a custom provider with a simple prompt.""" + print(f"\n--- Testing {provider_name} ---\n") + + client = CopilotClient() + + try: + await client.start() + session = await client.create_session({"provider": provider_config}) + + response = None + + def handler(event): + nonlocal response + if event.type == SessionEventType.ASSISTANT_MESSAGE: + response = event.data.content + + session.on(handler) + + # Test with a simple prompt + await session.send_and_wait( + {"prompt": "Say 'Hello from custom provider' and nothing else."}, + timeout=60.0, + ) + + if response: + print(f"βœ“ Response: {response}") + else: + print("βœ— No response received") + + await session.destroy() + + except Exception as e: + print(f"βœ— Error: {e}") + + finally: + await client.stop() + + +async def demo_openai(): + """Demonstrate using OpenAI as the provider.""" + try: + provider = get_openai_provider() + await demo_with_provider("OpenAI", provider) + except ValueError as e: + print(f"\n⚠️ Skipping OpenAI demo: {e}") + + +async def demo_azure(): + """Demonstrate using Azure OpenAI as the provider.""" + try: + provider = get_azure_openai_provider() + await demo_with_provider("Azure OpenAI", provider) + except ValueError as e: + print(f"\n⚠️ Skipping Azure demo: {e}") + + +async def demo_anthropic(): + """Demonstrate using Anthropic as the provider.""" + try: + provider = get_anthropic_provider() + await demo_with_provider("Anthropic Claude", provider) + except ValueError as e: + print(f"\n⚠️ Skipping Anthropic demo: {e}") + + +# ============================================================================= +# Provider Switching +# ============================================================================= + + +async def demo_provider_switching(): + """Switch between providers for different tasks.""" + print("\n=== Provider Switching Demo ===\n") + + client = CopilotClient() + + try: + await client.start() + + available_providers = [] + if os.environ.get("OPENAI_API_KEY"): + available_providers.append(("OpenAI", get_openai_provider())) + if os.environ.get("ANTHROPIC_API_KEY"): + available_providers.append(("Anthropic", get_anthropic_provider())) + + if not available_providers: + print("No custom providers configured.") + return + + sessions = {} + for name, provider in available_providers: + session = await client.create_session({"provider": provider}) + sessions[name] = session + print(f"βœ“ Created session with {name}") + + for name, session in sessions.items(): + response = None + + def handler(event): + nonlocal response + if event.type == SessionEventType.ASSISTANT_MESSAGE: + response = event.data.content + + session.on(handler) + await session.send_and_wait( + {"prompt": f"You are {name}. Say 'Hello!'"}, + timeout=60.0, + ) + print(f"\n{name}: {response}") + await session.destroy() + + finally: + await client.stop() + + +# ============================================================================= +# Fallback Pattern +# ============================================================================= + + +async def demo_fallback_pattern(): + """Fallback pattern - try primary provider, fall back to secondary.""" + print("\n=== Fallback Pattern Demo ===\n") + + providers = [] + if os.environ.get("OPENAI_API_KEY"): + providers.append(("OpenAI", get_openai_provider())) + if os.environ.get("ANTHROPIC_API_KEY"): + providers.append(("Anthropic", get_anthropic_provider())) + + if not providers: + print("No providers available. Set API keys to test fallback pattern.") + return + + print(f"Provider priority: {[p[0] for p in providers]}") + + client = CopilotClient() + + try: + await client.start() + + for name, provider in providers: + print(f"\nTrying {name}...") + try: + session = await client.create_session({"provider": provider}) + + response = None + + def handler(event): + nonlocal response + if event.type == SessionEventType.ASSISTANT_MESSAGE: + response = event.data.content + + session.on(handler) + await session.send_and_wait( + {"prompt": "Say 'Success!' and nothing else."}, + timeout=30.0, + ) + await session.destroy() + + if response: + print(f"βœ“ {name} succeeded: {response}") + break # Success, no need to try others + + except Exception as e: + print(f"βœ— {name} failed: {e}") + continue # Try next provider + + else: + print("\nβœ— All providers failed!") + + finally: + await client.stop() + + +# ============================================================================= +# Configuration Guide +# ============================================================================= + + +def print_configuration_guide(): + """Print setup instructions for custom providers.""" + print(""" +CONFIGURATION GUIDE - Bring Your Own Key (BYOK) + +Set environment variables for your provider: + + OpenAI: export OPENAI_API_KEY="sk-..." + Azure: export AZURE_OPENAI_API_KEY="..." + export AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com" + Anthropic: export ANTHROPIC_API_KEY="sk-ant-..." +""") + + +# ============================================================================= +# Main +# ============================================================================= + + +async def main(): + """Run BYOK demonstrations.""" + print("=" * 60) + print("Custom Providers (BYOK)") + print("=" * 60) + + print_configuration_guide() + + has_openai = bool(os.environ.get("OPENAI_API_KEY")) + has_azure = bool(os.environ.get("AZURE_OPENAI_API_KEY")) + has_anthropic = bool(os.environ.get("ANTHROPIC_API_KEY")) + + print("Detected providers:") + print(f" OpenAI: {'βœ“' if has_openai else 'βœ—'}") + print(f" Azure: {'βœ“' if has_azure else 'βœ—'}") + print(f" Anthropic: {'βœ“' if has_anthropic else 'βœ—'}") + + if not any([has_openai, has_azure, has_anthropic]): + print("\n⚠️ No API keys found. Set environment variables to test.") + return + + await demo_openai() + await demo_azure() + await demo_anthropic() + await demo_provider_switching() + await demo_fallback_pattern() + + print("\n" + "=" * 60) + print("All demos completed!") + print("=" * 60) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/cookbook/python/recipe/custom_tools.py b/cookbook/python/recipe/custom_tools.py new file mode 100644 index 00000000..b2fede51 --- /dev/null +++ b/cookbook/python/recipe/custom_tools.py @@ -0,0 +1,434 @@ +#!/usr/bin/env python3 +""" +Custom Tools - Defining and using custom tools with the Copilot SDK. +Run: python custom_tools.py +""" + +import asyncio +import json +import random +from datetime import datetime + +from pydantic import BaseModel, Field + +from copilot import CopilotClient +from copilot.tools import define_tool +from copilot.types import SessionEventType, ToolResult + + +# ============================================================================= +# Simple Tools with @define_tool Decorator +# ============================================================================= + + +class GetWeatherParams(BaseModel): + """Parameters for the get_weather tool.""" + + city: str = Field(description="The city name to get weather for") + units: str = Field( + default="celsius", + description="Temperature units: 'celsius' or 'fahrenheit'", + ) + + +@define_tool(description="Get the current weather for a city") +def get_weather(params): + """Simulated weather API. In production, call a real weather API.""" + weather_data = { + "temperature": random.randint(15, 30), + "condition": random.choice(["sunny", "cloudy", "rainy", "partly cloudy"]), + "humidity": random.randint(40, 80), + "wind_speed": random.randint(5, 25), + } + + if params.units == "fahrenheit": + weather_data["temperature"] = int(weather_data["temperature"] * 9 / 5 + 32) + temp_unit = "Β°F" + else: + temp_unit = "Β°C" + + return ( + f"Weather in {params.city}:\n" + f" Temperature: {weather_data['temperature']}{temp_unit}\n" + f" Condition: {weather_data['condition']}\n" + f" Humidity: {weather_data['humidity']}%\n" + f" Wind: {weather_data['wind_speed']} km/h" + ) + + +class CalculatorParams(BaseModel): + """Parameters for the calculator tool.""" + + expression: str = Field(description="Mathematical expression to evaluate") + + +@define_tool(description="Evaluate a mathematical expression safely") +def calculator(params): + """Safely evaluate mathematical expressions.""" + allowed = set("0123456789+-*/().% ") + + if not all(c in allowed for c in params.expression): + return "Error: Expression contains invalid characters" + + try: + # Use eval with restricted globals for safety + result = eval(params.expression, {"__builtins__": {}}, {}) + return f"Result: {result}" + except Exception as e: + return f"Error: {e}" + + +class GetTimeParams(BaseModel): + """Parameters for the get_time tool.""" + + timezone: str = Field(default="UTC", description="Timezone name") + + +@define_tool(description="Get the current time in a specific timezone") +def get_current_time(params): + """Get the current time.""" + now = datetime.now() + return f"Current time ({params.timezone}): {now.strftime('%Y-%m-%d %H:%M:%S')}" + + +# ============================================================================= +# Async Tool +# ============================================================================= + + +class FetchURLParams(BaseModel): + """Parameters for the fetch_url tool.""" + + url: str = Field(description="The URL to fetch content from") + max_length: int = Field(default=1000, description="Maximum characters to return") + + +@define_tool(description="Fetch content from a URL (simulated)") +async def fetch_url(params): + """Async tool for fetching URL content.""" + await asyncio.sleep(0.5) # Simulate network delay + + # Simulated response + content = f""" + + +Content from {params.url} + +

Fetched Content

+

This is simulated content from {params.url}

+

In a real implementation, this would be the actual page content.

+ + +""" + + if len(content) > params.max_length: + content = content[: params.max_length] + "... (truncated)" + + return content + + +# ============================================================================= +# Tool with Invocation Context +# ============================================================================= + + +class LogMessageParams(BaseModel): + """Parameters for the log_message tool.""" + + level: str = Field(description="Log level: 'info', 'warning', 'error'") + message: str = Field(description="The message to log") + + +@define_tool(description="Log a message with context information") +def log_message(params, invocation): + """Tool that accesses the invocation context.""" + log_entry = { + "timestamp": datetime.now().isoformat(), + "level": params.level.upper(), + "message": params.message, + "session_id": invocation["session_id"][:12] + "...", + "tool_call_id": invocation["tool_call_id"][:8] + "...", + } + + print(f"[LOG] {json.dumps(log_entry, indent=2)}") + return f"Logged {params.level} message: {params.message}" + + +# ============================================================================= +# Tool Returning Structured Data +# ============================================================================= + + +class SearchParams(BaseModel): + """Parameters for the search_database tool.""" + + query: str = Field(description="Search query string") + limit: int = Field(default=5, description="Maximum results") + + +class SearchResult(BaseModel): + """A single search result.""" + + id: str + title: str + score: float + snippet: str + + +@define_tool(description="Search a database of documents (simulated)") +def search_database(params): + """Tool that returns structured Pydantic models.""" + results = [] + for i in range(min(params.limit, 5)): + results.append( + SearchResult( + id=f"doc-{random.randint(1000, 9999)}", + title=f"Document about {params.query} ({i + 1})", + score=random.uniform(0.7, 1.0), + snippet=f"This document discusses {params.query} in detail...", + ) + ) + return results + + +# ============================================================================= +# Tool with Custom Result +# ============================================================================= + + +class FormatDataParams(BaseModel): + """Parameters for the format_data tool.""" + + data: dict = Field(description="The data to format") + format: str = Field(default="json", description="Output format: json, table, markdown") + + +@define_tool(description="Format data in various output formats") +def format_data(params): + """Tool that returns a custom ToolResult.""" + data = params.data + + if params.format == "json": + formatted = json.dumps(data, indent=2) + elif params.format == "table": + lines = [] + for key, value in data.items(): + lines.append(f"| {key:20} | {str(value):30} |") + formatted = "\n".join(lines) + elif params.format == "markdown": + lines = [f"- **{key}**: {value}" for key, value in data.items()] + formatted = "\n".join(lines) + else: + return ToolResult( + textResultForLlm=f"Unknown format: {params.format}", + resultType="failure", + error=f"Unknown format: {params.format}", + ) + + return ToolResult( + textResultForLlm=formatted, + resultType="success", + ) + + +# ============================================================================= +# Tool Without Decorator +# ============================================================================= + + +def create_greeting_tool(): + """Create a tool using the functional API instead of decorators.""" + + class GreetParams(BaseModel): + name: str = Field(description="Name of the person to greet") + language: str = Field(default="en", description="Language code: en, es, fr, de") + + def handler(params): + greetings = { + "en": f"Hello, {params.name}!", + "es": f"Β‘Hola, {params.name}!", + "fr": f"Bonjour, {params.name}!", + "de": f"Hallo, {params.name}!", + } + return greetings.get(params.language, greetings["en"]) + + return define_tool( + "greet_user", + description="Greet a user in their preferred language", + handler=handler, + params_type=GreetParams, + ) + + +# ============================================================================= +# Demo: Basic Tools +# ============================================================================= + + +async def demo_basic_tools(): + """Demonstrate basic custom tools.""" + print("\n=== Basic Custom Tools ===\n") + + client = CopilotClient() + + try: + await client.start() + + # Create session with custom tools + tools = [get_weather, calculator, get_current_time] + session = await client.create_session({"tools": tools}) + + def handler(event): + if event.type == SessionEventType.ASSISTANT_MESSAGE: + print(f"\nπŸ€– {event.data.content}") + elif event.type == SessionEventType.TOOL_EXECUTION_START: + print(f" βš™οΈ Executing: {event.data.tool_name}") + elif event.type == SessionEventType.TOOL_EXECUTION_COMPLETE: + print(f" βœ“ Completed") + + session.on(handler) + + # Ask questions that use the tools + print("Asking about weather...") + await session.send_and_wait( + {"prompt": "What's the weather like in Tokyo and New York?"}, + timeout=60.0, + ) + + print("\n" + "-" * 40) + print("Asking for calculations...") + await session.send_and_wait( + {"prompt": "Calculate: 15% of 250, and also (125 * 4) / 5"}, + timeout=60.0, + ) + + print("\n" + "-" * 40) + print("Asking for time...") + await session.send_and_wait( + {"prompt": "What time is it right now?"}, + timeout=60.0, + ) + + await session.destroy() + + finally: + await client.stop() + + +async def demo_advanced_tools(): + """Demonstrate advanced custom tools.""" + print("\n=== Advanced Custom Tools ===\n") + + client = CopilotClient() + + try: + await client.start() + + tools = [fetch_url, search_database, format_data, log_message, create_greeting_tool()] + session = await client.create_session({"tools": tools}) + + def handler(event): + if event.type == SessionEventType.ASSISTANT_MESSAGE: + print(f"\nπŸ€– {event.data.content}") + elif event.type == SessionEventType.TOOL_EXECUTION_START: + print(f" βš™οΈ Executing: {event.data.tool_name}") + + session.on(handler) + + # Test structured data tool + print("Testing search tool...") + await session.send_and_wait( + {"prompt": "Search for documents about 'machine learning'"}, + timeout=60.0, + ) + + print("\n" + "-" * 40) + print("Testing greeting tool...") + await session.send_and_wait( + {"prompt": "Greet Alice in French and Bob in Spanish"}, + timeout=60.0, + ) + + print("\n" + "-" * 40) + print("Testing format tool...") + await session.send_and_wait( + { + "prompt": "Format this data as markdown: name=John, age=30, city=NYC" + }, + timeout=60.0, + ) + + await session.destroy() + + finally: + await client.stop() + + +async def demo_tool_orchestration(): + """Demonstrate multiple tools working together.""" + print("\n=== Tool Orchestration ===\n") + + client = CopilotClient() + + try: + await client.start() + + tools = [get_weather, calculator, get_current_time, search_database, log_message] + session = await client.create_session({"tools": tools}) + + def handler(event): + if event.type == SessionEventType.ASSISTANT_MESSAGE: + print(f"\nπŸ€– {event.data.content}") + elif event.type == SessionEventType.TOOL_EXECUTION_START: + print(f" βš™οΈ {event.data.tool_name}") + elif event.type == SessionEventType.TOOL_EXECUTION_COMPLETE: + print(f" βœ“ Done") + + session.on(handler) + + # Complex request requiring multiple tools + print("Complex request using multiple tools...") + await session.send_and_wait( + { + "prompt": """ +Please help me with a few things: +1. What's the current time? +2. What's the weather in London? +3. Search for documents about 'Python' +4. Log an info message saying 'User requested status check' +5. Calculate: what's 20% of the number 350? + +Summarize all findings at the end. +""" + }, + timeout=120.0, + ) + + await session.destroy() + + finally: + await client.stop() + + +# ============================================================================= +# Main +# ============================================================================= + + +async def main(): + """Run all custom tools demonstrations.""" + print("=" * 60) + print("Custom Tools Patterns") + print("=" * 60) + + await demo_basic_tools() + await demo_advanced_tools() + await demo_tool_orchestration() + + print("\n" + "=" * 60) + print("All demos completed!") + print("=" * 60) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/cookbook/python/recipe/error_handling.py b/cookbook/python/recipe/error_handling.py index 57073037..dbaf5aac 100644 --- a/cookbook/python/recipe/error_handling.py +++ b/cookbook/python/recipe/error_handling.py @@ -1,28 +1,258 @@ #!/usr/bin/env python3 +""" +Error Handling Patterns for the Copilot SDK. + +Run: python error_handling.py +""" + +import asyncio +import signal +import sys from copilot import CopilotClient +from copilot.types import SessionEventType + -client = CopilotClient() +# ============================================================================= +# Basic Error Handling +# ============================================================================= -try: - client.start() - session = client.create_session(model="gpt-5") +async def basic_error_handling(): + """Simple try-except-finally pattern.""" + print("\n=== Basic Error Handling ===\n") + + client = CopilotClient() + session = None response = None - def handle_message(event): + + def handle_event(event): nonlocal response - if event["type"] == "assistant.message": - response = event["data"]["content"] + if event.type == SessionEventType.ASSISTANT_MESSAGE: + response = event.data.content + + try: + await client.start() + print("βœ“ Client connected") + + session = await client.create_session() + session.on(handle_event) + print(f"βœ“ Session: {session.session_id[:12]}...") + + await session.send_and_wait({"prompt": "Say hello."}, timeout=30.0) + + if response: + print(f"βœ“ Response: {response}") + + except FileNotFoundError: + print("βœ— Copilot CLI not found") + + except ConnectionError as e: + print(f"βœ— Connection Error: {e}") + + except asyncio.TimeoutError: + print("βœ— Request timed out") + + except Exception as e: + print(f"βœ— Error: {type(e).__name__}: {e}") + + finally: + if session: + try: + await session.destroy() + except Exception: + pass + await client.stop() + print("βœ“ Cleanup complete") + + +# ============================================================================= +# Context Manager Pattern +# ============================================================================= + + +class CopilotContext: + """Context manager for automatic cleanup.""" + + def __init__(self, **options): + self.client = CopilotClient(options if options else None) + self.session = None + + async def __aenter__(self): + await self.client.start() + self.session = await self.client.create_session() + return self.client, self.session + + async def __aexit__(self, exc_type, exc_val, exc_tb): + if self.session: + try: + await self.session.destroy() + except Exception: + pass + await self.client.stop() + return False + + +async def context_manager_demo(): + """Use context manager for automatic cleanup.""" + print("\n=== Context Manager Pattern ===\n") + + try: + async with CopilotContext() as (client, session): + print(f"βœ“ Session: {session.session_id[:12]}...") + await session.send_and_wait({"prompt": "What is 2+2?"}, timeout=30.0) + print("βœ“ Request completed") + + print("βœ“ Auto cleanup done") + + except Exception as e: + print(f"βœ— Error: {e}") + + +# ============================================================================= +# Retry with Backoff +# ============================================================================= + + +async def retry_with_backoff(func, max_retries=3, base_delay=1.0): + """Retry with exponential backoff.""" + last_error = None + + for attempt in range(max_retries + 1): + try: + return await func() + except (ConnectionError, asyncio.TimeoutError) as e: + last_error = e + if attempt < max_retries: + delay = min(base_delay * (2 ** attempt), 30.0) + print(f" Retry {attempt + 1}/{max_retries} in {delay:.1f}s...") + await asyncio.sleep(delay) + + if last_error is None: + raise RuntimeError("Max retries exceeded without a captured error.") + raise last_error + + +async def retry_demo(): + """Demonstrate retry pattern.""" + print("\n=== Retry Pattern ===\n") + + async def make_request(): + async with CopilotContext() as (_, session): + await session.send_and_wait({"prompt": "Hello!"}, timeout=30.0) + return "Success!" + + try: + result = await retry_with_backoff(make_request) + print(f"βœ“ {result}") + except Exception as e: + print(f"βœ— All retries failed: {e}") + + +# ============================================================================= +# Timeout and Abort +# ============================================================================= + + +async def timeout_demo(): + """Handle timeouts with abort.""" + print("\n=== Timeout Handling ===\n") + + client = CopilotClient() + + try: + await client.start() + session = await client.create_session() + + try: + await session.send_and_wait( + {"prompt": "Write a long essay."}, + timeout=5.0 + ) + print("βœ“ Completed") + + except asyncio.TimeoutError: + print("⚠ Timeout - aborting...") + await session.abort() + print("βœ“ Aborted") + + await session.destroy() + + finally: + await client.stop() + + +# ============================================================================= +# Graceful Shutdown +# ============================================================================= + + +class GracefulShutdown: + """Handle Ctrl+C gracefully.""" + + def __init__(self): + self.shutdown_event = asyncio.Event() + self.client = None + self.session = None + + def setup_signals(self): + def handler(sig, frame=None): + print("\n⚠ Shutdown requested...") + self.shutdown_event.set() + + signal.signal(signal.SIGINT, handler) + if sys.platform != "win32": + signal.signal(signal.SIGTERM, handler) + + async def run(self): + self.setup_signals() + + self.client = CopilotClient() + await self.client.start() + self.session = await self.client.create_session() + + print("Running... Press Ctrl+C to stop") + + try: + while not self.shutdown_event.is_set(): + await asyncio.sleep(0.5) + finally: + await self.cleanup() + + async def cleanup(self): + if self.session: + await self.session.destroy() + if self.client: + await self.client.stop() + print("βœ“ Shutdown complete") + + +async def graceful_shutdown_demo(): + """Show graceful shutdown pattern.""" + print("\n=== Graceful Shutdown Pattern ===\n") + print("See GracefulShutdown class for implementation.") + print("βœ“ Pattern documented") + + +# ============================================================================= +# Main +# ============================================================================= + + +async def main(): + print("=" * 50) + print("ERROR HANDLING PATTERNS") + print("=" * 50) + + await basic_error_handling() + await context_manager_demo() + await retry_demo() + await timeout_demo() + await graceful_shutdown_demo() - session.on(handle_message) - session.send(prompt="Hello!") - session.wait_for_idle() + print("\n" + "=" * 50) + print("All demos completed!") - if response: - print(response) - session.destroy() -except Exception as e: - print(f"Error: {e}") -finally: - client.stop() +if __name__ == "__main__": + asyncio.run(main()) diff --git a/cookbook/python/recipe/managing_local_files.py b/cookbook/python/recipe/managing_local_files.py index 0fd43e50..0115c0f3 100644 --- a/cookbook/python/recipe/managing_local_files.py +++ b/cookbook/python/recipe/managing_local_files.py @@ -1,42 +1,239 @@ #!/usr/bin/env python3 +""" +Managing Local Files - Using Copilot to organize and manage files. +Run: python managing_local_files.py [--quick] +""" -from copilot import CopilotClient +import asyncio import os +import sys +from pathlib import Path + +from copilot import CopilotClient +from copilot.types import SessionEventType + + +# ============================================================================= +# Event Handler +# ============================================================================= + + +def create_event_handler(verbose=True): + """Create an event handler for progress display.""" + + def handle_event(event): + if event.type == SessionEventType.ASSISTANT_MESSAGE: + print(f"\nπŸ€– Copilot:\n{event.data.content}\n") + elif event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA and verbose: + delta = getattr(event.data, "delta_content", "") + if delta: + print(delta, end="", flush=True) + elif event.type == SessionEventType.TOOL_EXECUTION_START: + print(f" βš™οΈ Starting: {event.data.tool_name}") + elif event.type == SessionEventType.TOOL_EXECUTION_COMPLETE: + print(f" βœ“ Completed") + elif event.type == SessionEventType.SESSION_ERROR: + message = getattr(event.data, "message", str(event.data)) + print(f" βœ— Error: {message}") + + return handle_event + + +# ============================================================================= +# Permission Handler +# ============================================================================= + + +def create_permission_handler(auto_approve=False): + """Create a permission handler for file operations.""" + + def handle_permission(request, context): + kind = request.get("kind", "unknown") + + if auto_approve: + print(f" πŸ”“ Auto-approved: {kind}") + return {"kind": "approved"} + + print(f"\n⚠️ Permission Request: {kind}") + try: + response = input(" Approve? (y/n): ").strip().lower() + if response in ("y", "yes"): + return {"kind": "approved"} + return {"kind": "denied-interactively-by-user"} + except (EOFError, KeyboardInterrupt): + return {"kind": "denied-interactively-by-user"} + + return handle_permission + + +# ============================================================================= +# File Organization Strategies +# ============================================================================= + + +async def organize_by_extension(session, target_folder, dry_run=True): + """Organize files by extension into subfolders.""" + action = "show me a preview of" if dry_run else "execute" + prompt = f""" +Analyze the files in "{target_folder}" and {action} organizing them by file extension. + +Grouping: images/, documents/, videos/, audio/, archives/, code/, data/, other/ +{"Only show the plan, DO NOT move any files." if dry_run else "Create folders and move files."} +""" + await session.send_and_wait({"prompt": prompt}, timeout=120.0) + + +async def organize_by_date(session, target_folder, dry_run=True): + """Organize files by modification date into year/month folders.""" + action = "show me a preview of" if dry_run else "execute" + prompt = f""" +Analyze the files in "{target_folder}" and {action} organizing them by modification date. + +Structure: year/month folders (e.g., "2024/01-January/") +{"Only show the plan, DO NOT move any files." if dry_run else "Create folders and move files."} +""" + await session.send_and_wait({"prompt": prompt}, timeout=120.0) + + +async def organize_by_size(session, target_folder, dry_run=True): + """Organize files by size into size-based folders.""" + action = "show me a preview of" if dry_run else "execute" + prompt = f""" +Analyze the files in "{target_folder}" and {action} organizing them by file size. + +Categories: tiny-under-1kb/, small-under-1mb/, medium-under-100mb/, large-under-1gb/, huge-over-1gb/ +{"Only show the plan, DO NOT move any files." if dry_run else "Create folders and move files."} +""" + await session.send_and_wait({"prompt": prompt}, timeout=120.0) + + +async def smart_organize(session, target_folder, dry_run=True): + """Let Copilot analyze and suggest the best organization strategy.""" + prompt = f""" +Analyze ALL files in "{target_folder}" and suggest the best organization. + +Consider file names, types, sizes, and patterns. +{"Show what files would go where (DO NOT move anything)" if dry_run else "Create folders and organize files"} +""" + await session.send_and_wait({"prompt": prompt}, timeout=180.0) + + +# ============================================================================= +# Interactive Demo +# ============================================================================= + + +async def interactive_demo(): + """Run an interactive file organization demo.""" + print("=" * 60) + print("πŸ“ Copilot File Organizer") + print("=" * 60) + + default_folder = os.path.expanduser("~/Downloads") + print(f"\nDefault folder: {default_folder}") + folder_input = input("Enter folder path (or Enter for default): ").strip() + target_folder = folder_input if folder_input else default_folder + + if not Path(target_folder).is_dir(): + print(f"βœ— Error: '{target_folder}' is not a valid directory") + return + + print("\nStrategies:") + print(" 1. By extension 2. By date 3. By size 4. Smart organize") + + strategy_input = input("Choose (1-4): ").strip() + strategies = { + "1": ("extension", organize_by_extension), + "2": ("date", organize_by_date), + "3": ("size", organize_by_size), + "4": ("smart", smart_organize), + } + strategy_name, strategy_func = strategies.get(strategy_input, strategies["1"]) + + dry_run = input("Dry run only? (Y/n): ").strip().lower() not in ("n", "no") + + print(f"\n{'πŸ“‹ DRY RUN' if dry_run else '⚠️ LIVE MODE'} | {target_folder} | {strategy_name}\n") + + client = CopilotClient({"log_level": "error"}) + + try: + await client.start() + session = await client.create_session({ + "on_permission_request": create_permission_handler(auto_approve=dry_run), + }) + session.on(create_event_handler(verbose=False)) + + await strategy_func(session, target_folder, dry_run) + + print("\n" + "-" * 40) + print("πŸ’‘ Ask follow-up questions or 'exit' to quit.") + + while True: + try: + user_input = input("\nYou: ").strip() + except (EOFError, KeyboardInterrupt): + print("\nπŸ‘‹ Goodbye!") + break + + if user_input.lower() in ("exit", "quit", "q"): + break + + if user_input: + await session.send_and_wait({"prompt": user_input}, timeout=120.0) + + await session.destroy() + + except Exception as e: + print(f"βœ— Error: {e}") + finally: + await client.stop() + + +# ============================================================================= +# Quick Start +# ============================================================================= + + +async def quick_start(): + """Minimal file organization example.""" + print("\n=== Quick Start: File Organization ===\n") + + client = CopilotClient() + + try: + await client.start() + session = await client.create_session() + + def handle_event(event): + if event.type == SessionEventType.ASSISTANT_MESSAGE: + print(f"\n{event.data.content}\n") -# Create and start client -client = CopilotClient() -client.start() + session.on(handle_event) -# Create session -session = client.create_session(model="gpt-5") + target = os.path.expanduser("~/Downloads") + await session.send_and_wait( + {"prompt": f"List 10 files in '{target}' and suggest organization. Don't move anything."}, + timeout=60.0, + ) -# Event handler -def handle_event(event): - if event["type"] == "assistant.message": - print(f"\nCopilot: {event['data']['content']}") - elif event["type"] == "tool.execution_start": - print(f" β†’ Running: {event['data']['toolName']}") - elif event["type"] == "tool.execution_complete": - print(f" βœ“ Completed: {event['data']['toolCallId']}") + await session.destroy() -session.on(handle_event) + finally: + await client.stop() -# Ask Copilot to organize files -# Change this to your target folder -target_folder = os.path.expanduser("~/Downloads") -session.send(prompt=f""" -Analyze the files in "{target_folder}" and organize them into subfolders. +# ============================================================================= +# Main +# ============================================================================= -1. First, list all files and their metadata -2. Preview grouping by file extension -3. Create appropriate subfolders (e.g., "images", "documents", "videos") -4. Move each file to its appropriate subfolder -Please confirm before moving any files. -""") +async def main(): + """Main entry point.""" + if len(sys.argv) > 1 and sys.argv[1] == "--quick": + await quick_start() + else: + await interactive_demo() -session.wait_for_idle() -session.destroy() -client.stop() +if __name__ == "__main__": + asyncio.run(main()) diff --git a/cookbook/python/recipe/mcp_servers.py b/cookbook/python/recipe/mcp_servers.py new file mode 100644 index 00000000..8718b7a4 --- /dev/null +++ b/cookbook/python/recipe/mcp_servers.py @@ -0,0 +1,361 @@ +#!/usr/bin/env python3 +""" +MCP Servers - Integrating Model Context Protocol servers with Copilot. +Run: python mcp_servers.py +""" + +import asyncio +import os + +from copilot import CopilotClient +from copilot.types import MCPLocalServerConfig, MCPRemoteServerConfig, SessionEventType + + +# ============================================================================= +# MCP Server Configurations +# ============================================================================= + + +def get_filesystem_mcp_server(): + """Configure the official filesystem MCP server.""" + allowed_dir = os.path.expanduser("~/Documents") + return MCPLocalServerConfig( + type="stdio", + command="npx", + args=["-y", "@modelcontextprotocol/server-filesystem", allowed_dir], + tools=["*"], + ) + + +def get_github_mcp_server(): + """Configure the GitHub MCP server. Requires GITHUB_TOKEN.""" + github_token = os.environ.get("GITHUB_TOKEN", "") + return MCPLocalServerConfig( + type="stdio", + command="npx", + args=["-y", "@modelcontextprotocol/server-github"], + env={"GITHUB_TOKEN": github_token}, + tools=["*"], + timeout=30000, + ) + + +def get_sqlite_mcp_server(db_path): + """Configure the SQLite MCP server.""" + return MCPLocalServerConfig( + type="stdio", + command="npx", + args=["-y", "@modelcontextprotocol/server-sqlite", db_path], + tools=["query", "list_tables", "describe_table"], + ) + + +def get_remote_mcp_server(): + """Configure a remote HTTP/SSE MCP server.""" + return MCPRemoteServerConfig( + type="sse", + url=os.environ.get("MCP_SERVER_URL", "http://localhost:3001/mcp"), + headers={"Authorization": f"Bearer {os.environ.get('MCP_SERVER_TOKEN', '')}"}, + tools=["*"], + timeout=10000, + ) + + +def get_custom_mcp_server(command, args, tools=None): + """Configure a custom MCP server.""" + return MCPLocalServerConfig( + type="stdio", + command=command, + args=args, + tools=tools or ["*"], + ) + + +# ============================================================================= +# Event Handler +# ============================================================================= + + +def create_mcp_event_handler(verbose=True): + """Create an event handler that highlights MCP tool usage.""" + + def handler(event): + if event.type == SessionEventType.ASSISTANT_MESSAGE: + print(f"\nπŸ€– {event.data.content}\n") + elif event.type == SessionEventType.TOOL_EXECUTION_START: + tool_name = event.data.tool_name + prefix = "πŸ”Œ MCP Tool:" if tool_name.startswith("mcp_") else "βš™οΈ Tool:" + print(f" {prefix} {tool_name}") + elif event.type == SessionEventType.TOOL_EXECUTION_COMPLETE: + print(" βœ“ Done") + elif event.type == SessionEventType.SESSION_ERROR: + message = getattr(event.data, "message", str(event.data)) + print(f" ❌ Error: {message}") + + return handler + + +# ============================================================================= +# Demo: GitHub MCP +# ============================================================================= + + +async def demo_github_mcp(): + """Demonstrate using the GitHub MCP server. Requires GITHUB_TOKEN.""" + print("\n=== GitHub MCP Server Demo ===\n") + + if not os.environ.get("GITHUB_TOKEN"): + print("⚠️ GITHUB_TOKEN not set. Skipping GitHub MCP demo.") + return + + client = CopilotClient() + + try: + await client.start() + + # Create session with GitHub MCP server + session = await client.create_session( + { + "mcp_servers": { + "github": get_github_mcp_server(), + }, + } + ) + + session.on(create_mcp_event_handler()) + + print("Using GitHub MCP server to analyze a repository...\n") + + await session.send_and_wait( + { + "prompt": """ +Search for the most popular Python repositories on GitHub. +Show me the top 3 by stars, with their descriptions. +""" + }, + timeout=120.0, + ) + + await session.destroy() + + except Exception as e: + print(f"Error: {e}") + + finally: + await client.stop() + + +# ============================================================================= +# Demo: Filesystem MCP +# ============================================================================= + + +async def demo_filesystem_mcp(): + """Demonstrate using the filesystem MCP server.""" + print("\n=== Filesystem MCP Server Demo ===\n") + + # Create a test directory + test_dir = os.path.expanduser("~/copilot-mcp-test") + os.makedirs(test_dir, exist_ok=True) + + # Create a test file + test_file = os.path.join(test_dir, "sample.txt") + with open(test_file, "w") as f: + f.write("Hello from the MCP demo!\nThis is a test file.") + + print(f"Test directory: {test_dir}") + print(f"Test file: {test_file}\n") + + client = CopilotClient() + + try: + await client.start() + + # Configure filesystem server for the test directory + fs_server = MCPLocalServerConfig( + type="stdio", + command="npx", + args=["-y", "@modelcontextprotocol/server-filesystem", test_dir], + tools=["*"], + ) + + session = await client.create_session( + { + "mcp_servers": { + "filesystem": fs_server, + }, + } + ) + + session.on(create_mcp_event_handler()) + + print("Using filesystem MCP server to explore files...\n") + + await session.send_and_wait( + { + "prompt": f""" +List the files in {test_dir} and read the content of sample.txt. +Then create a new file called 'created_by_copilot.txt' with a greeting message. +""" + }, + timeout=120.0, + ) + + await session.destroy() + + except Exception as e: + print(f"Error: {e}") + print("Note: Make sure the filesystem MCP server is installed:") + print(" npx -y @modelcontextprotocol/server-filesystem --help") + + finally: + await client.stop() + + # Cleanup + import shutil + + if os.path.exists(test_dir): + shutil.rmtree(test_dir) + print(f"\nCleaned up test directory: {test_dir}") + + +# ============================================================================= +# Demo: Multiple MCP Servers +# ============================================================================= + + +async def demo_multiple_mcp_servers(): + """Demonstrate using multiple MCP servers together.""" + print("\n=== Multiple MCP Servers Demo ===\n") + + # Create test directory for filesystem server + test_dir = os.path.expanduser("~/copilot-mcp-multi-test") + os.makedirs(test_dir, exist_ok=True) + + client = CopilotClient() + + try: + await client.start() + + # Configure multiple MCP servers + mcp_servers = { + "filesystem": MCPLocalServerConfig( + type="stdio", + command="npx", + args=["-y", "@modelcontextprotocol/server-filesystem", test_dir], + tools=["*"], + ), + } + + # Add GitHub if token is available + if os.environ.get("GITHUB_TOKEN"): + mcp_servers["github"] = get_github_mcp_server() + + print(f"Configured {len(mcp_servers)} MCP server(s): {list(mcp_servers.keys())}") + + session = await client.create_session( + { + "mcp_servers": mcp_servers, + } + ) + + session.on(create_mcp_event_handler()) + + # Create a prompt that uses multiple servers + prompt = f"List files in {test_dir} using the filesystem tools." + + if "github" in mcp_servers: + prompt += " Also, show me 1 trending Python repository from GitHub." + + await session.send_and_wait({"prompt": prompt}, timeout=120.0) + + await session.destroy() + + except Exception as e: + print(f"Error: {e}") + + finally: + await client.stop() + + # Cleanup + import shutil + + if os.path.exists(test_dir): + shutil.rmtree(test_dir) + + +# ============================================================================= +# Demo: Tool Filtering +# ============================================================================= + + +async def demo_tool_filtering(): + """Demonstrate filtering which MCP tools are available.""" + print("\n=== MCP Tool Filtering Demo ===\n") + + print("Tool filtering options:") + print(" ['*'] - All tools") + print(" ['tool1', 'tool2'] - Only specific tools") + print(" [] - No tools (disabled)") + + # Example: read-only filesystem + read_only_config = MCPLocalServerConfig( + type="stdio", + command="npx", + args=["-y", "@modelcontextprotocol/server-filesystem", os.getcwd()], + tools=["read_file", "list_directory", "get_file_info"], + ) + + print(f"\nConfigured read-only filesystem: {read_only_config.get('tools')}") + print("βœ“ Tool filtering configured") + + +# ============================================================================= +# MCP Guide +# ============================================================================= + + +def print_mcp_guide(): + """Print a quick guide for MCP servers.""" + print(""" +MCP (Model Context Protocol) GUIDE + +Common MCP Servers: + @modelcontextprotocol/server-filesystem - File operations + @modelcontextprotocol/server-github - GitHub API + @modelcontextprotocol/server-sqlite - SQLite queries + @modelcontextprotocol/server-slack - Slack integration + +Install: npx -y @modelcontextprotocol/server- --help +Docs: https://modelcontextprotocol.io/docs/servers/building +""") + + +# ============================================================================= +# Main +# ============================================================================= + + +async def main(): + """Run MCP demonstrations.""" + print("=" * 60) + print("MCP Servers") + print("=" * 60) + + print_mcp_guide() + + print("Environment:") + print(f" GITHUB_TOKEN: {'βœ“' if os.environ.get('GITHUB_TOKEN') else 'βœ—'}") + + await demo_tool_filtering() + await demo_filesystem_mcp() + await demo_github_mcp() + await demo_multiple_mcp_servers() + + print("\n" + "=" * 60) + print("All demos completed!") + print("=" * 60) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/cookbook/python/recipe/multiple_sessions.py b/cookbook/python/recipe/multiple_sessions.py index 92921d2d..ff62e468 100644 --- a/cookbook/python/recipe/multiple_sessions.py +++ b/cookbook/python/recipe/multiple_sessions.py @@ -1,35 +1,235 @@ #!/usr/bin/env python3 +""" +Multiple Sessions - Managing independent conversations. + +Run: python multiple_sessions.py +""" + +import asyncio from copilot import CopilotClient +from copilot.types import SessionEventType + + +# ============================================================================= +# Basic Multiple Sessions +# ============================================================================= + + +async def basic_multiple_sessions(): + """Create and use multiple independent sessions.""" + print("\n=== Basic Multiple Sessions ===\n") + + client = CopilotClient() + + try: + await client.start() + + # Create independent sessions + session1 = await client.create_session() + session2 = await client.create_session() + session3 = await client.create_session({"model": "claude-sonnet-4"}) + + print(f"Session 1: {session1.session_id[:12]}...") + print(f"Session 2: {session2.session_id[:12]}...") + print(f"Session 3: {session3.session_id[:12]}...") + + # Each session has its own context + await session1.send_and_wait({"prompt": "You help with Python."}) + await session2.send_and_wait({"prompt": "You help with JavaScript."}) + await session3.send_and_wait({"prompt": "You help with Go."}) + + print("βœ“ Context established for all sessions") + + # Ask context-aware questions + responses = {} + + def make_handler(name): + def handler(event): + if event.type == SessionEventType.ASSISTANT_MESSAGE: + responses[name] = event.data.content[:80] + "..." + return handler + + session1.on(make_handler("Python")) + session2.on(make_handler("JavaScript")) + session3.on(make_handler("Go")) + + await session1.send_and_wait({"prompt": "How do I create a virtual env?"}) + await session2.send_and_wait({"prompt": "How do I set up package.json?"}) + await session3.send_and_wait({"prompt": "How do I initialize a module?"}) + + print("\nResponses:") + for name, response in responses.items(): + print(f" {name}: {response}") + + await session1.destroy() + await session2.destroy() + await session3.destroy() + + finally: + await client.stop() + + +# ============================================================================= +# Parallel Execution +# ============================================================================= + + +async def parallel_execution(): + """Execute requests across sessions in parallel.""" + print("\n=== Parallel Execution ===\n") + + client = CopilotClient() + + try: + await client.start() + + topics = ["recursion", "polymorphism", "encapsulation"] + sessions = [await client.create_session() for _ in topics] + results = {} + + def make_handler(topic): + def handler(event): + if event.type == SessionEventType.ASSISTANT_MESSAGE: + results[topic] = event.data.content + return handler + + for session, topic in zip(sessions, topics): + session.on(make_handler(topic)) + + # Execute all in parallel + print("Executing requests in parallel...") + await asyncio.gather(*[ + s.send_and_wait({"prompt": f"Define {t} in one sentence."}) + for s, t in zip(sessions, topics) + ]) + + print("\nResults:") + for topic, response in results.items(): + print(f" {topic}: {response[:80]}...") + + for session in sessions: + await session.destroy() + + finally: + await client.stop() + + +# ============================================================================= +# Custom Session IDs +# ============================================================================= + + +async def custom_session_ids(): + """Use custom IDs for session management.""" + print("\n=== Custom Session IDs ===\n") + + client = CopilotClient() + + try: + await client.start() + + session_a = await client.create_session({"session_id": "user-42-support"}) + session_b = await client.create_session({"session_id": "user-42-dev"}) + + print(f"Created: {session_a.session_id}") + print(f"Created: {session_b.session_id}") + + await session_a.send_and_wait({"prompt": "I need help with a bug."}) + await session_b.send_and_wait({"prompt": "Let's design a feature."}) + + # List sessions + all_sessions = await client.list_sessions() + print(f"\nTotal sessions: {len(all_sessions)}") + + await session_a.destroy() + await session_b.destroy() + + finally: + await client.stop() + + +# ============================================================================= +# Session Pool +# ============================================================================= + + +class SessionPool: + """Reusable pool of sessions.""" + + def __init__(self, client, size=3): + self.client = client + self.size = size + self._available = asyncio.Queue() + self._sessions = [] + + async def initialize(self): + for i in range(self.size): + session = await self.client.create_session({"session_id": f"pool-{i}"}) + self._sessions.append(session) + await self._available.put(session) + print(f"βœ“ Pool initialized with {self.size} sessions") + + async def acquire(self, timeout=30.0): + return await asyncio.wait_for(self._available.get(), timeout) + + async def release(self, session): + await self._available.put(session) + + async def close(self): + for s in self._sessions: + await s.destroy() + + +async def session_pool_demo(): + """Demo session pool pattern.""" + print("\n=== Session Pool ===\n") + + client = CopilotClient() + + try: + await client.start() + + pool = SessionPool(client, size=2) + await pool.initialize() + + async def make_request(n): + session = await pool.acquire() + try: + await session.send_and_wait({"prompt": f"Say 'request {n}'"}) + print(f" Request {n} completed") + finally: + await pool.release(session) + + # Process 4 requests through 2 sessions + print("Processing 4 requests through 2 sessions...") + await asyncio.gather(*[make_request(i) for i in range(4)]) + + await pool.close() + print("βœ“ Pool closed") -client = CopilotClient() -client.start() + finally: + await client.stop() -# Create multiple independent sessions -session1 = client.create_session(model="gpt-5") -session2 = client.create_session(model="gpt-5") -session3 = client.create_session(model="claude-sonnet-4.5") -print("Created 3 independent sessions") +# ============================================================================= +# Main +# ============================================================================= -# Each session maintains its own conversation history -session1.send(prompt="You are helping with a Python project") -session2.send(prompt="You are helping with a TypeScript project") -session3.send(prompt="You are helping with a Go project") -print("Sent initial context to all sessions") +async def main(): + print("=" * 50) + print("MULTIPLE SESSIONS") + print("=" * 50) -# Follow-up messages stay in their respective contexts -session1.send(prompt="How do I create a virtual environment?") -session2.send(prompt="How do I set up tsconfig?") -session3.send(prompt="How do I initialize a module?") + await basic_multiple_sessions() + await parallel_execution() + await custom_session_ids() + await session_pool_demo() -print("Sent follow-up questions to each session") + print("\n" + "=" * 50) + print("All demos completed!") -# Clean up all sessions -session1.destroy() -session2.destroy() -session3.destroy() -client.stop() -print("All sessions destroyed successfully") +if __name__ == "__main__": + asyncio.run(main()) diff --git a/cookbook/python/recipe/persisting_sessions.py b/cookbook/python/recipe/persisting_sessions.py index 071ff1a8..cf0245e8 100644 --- a/cookbook/python/recipe/persisting_sessions.py +++ b/cookbook/python/recipe/persisting_sessions.py @@ -1,36 +1,383 @@ #!/usr/bin/env python3 +""" +Session Persistence - Demonstrates saving and resuming conversation sessions. +Run: python persisting_sessions.py +""" + +import asyncio +from datetime import datetime from copilot import CopilotClient +from copilot.types import SessionEventType + + +# ============================================================================= +# Basic Session Persistence +# ============================================================================= + + +async def basic_persistence(): + """Create and resume sessions with full history.""" + print("\n=== Basic Session Persistence ===\n") + + client = CopilotClient() + + try: + await client.start() + + # Create a session with a memorable ID + session_id = f"demo-session-{datetime.now().strftime('%H%M%S')}" + session = await client.create_session({"session_id": session_id}) + + print(f"βœ“ Created session: {session.session_id}") + + # Send a message to establish context + response_content = None + + def handler(event): + nonlocal response_content + if event.type == SessionEventType.ASSISTANT_MESSAGE: + response_content = event.data.content + + session.on(handler) + + await session.send_and_wait( + {"prompt": "Remember this: The secret code is ALPHA-7. What is it?"}, + timeout=60.0, + ) + + if response_content: + print(f" Response: {response_content[:100]}...") + + # Destroy session (disconnects but keeps data on disk) + await session.destroy() + print("βœ“ Session destroyed (data persisted to disk)") + + # Resume the session - all history is restored + print("\nResuming session...") + resumed = await client.resume_session(session_id) + + # Set up handler for resumed session + response_content = None + resumed.on(handler) + + print(f"βœ“ Resumed session: {resumed.session_id}") + + # Ask about the previous context - it should remember! + await resumed.send_and_wait( + {"prompt": "What was the secret code I mentioned earlier?"}, + timeout=60.0, + ) + + if response_content: + print(f" Response: {response_content}") + + # Get session history + messages = await resumed.get_messages() + print(f"\nβœ“ Session has {len(messages)} events in history") + + await resumed.destroy() + + # Delete session permanently + await client.delete_session(session_id) + print(f"βœ“ Session '{session_id}' deleted permanently") + + finally: + await client.stop() + + +# ============================================================================= +# Session Management +# ============================================================================= + + +async def session_management(): + """Manage multiple persistent sessions.""" + print("\n=== Session Management ===\n") + + client = CopilotClient() + + try: + await client.start() + + # Create several sessions + sessions_to_create = [ + ("user-alice-support", "I need help with Python decorators"), + ("user-bob-dev", "Let's design a REST API"), + ("project-demo", "This is a demo session"), + ] + + print("Creating multiple sessions...") + for session_id, initial_message in sessions_to_create: + session = await client.create_session({"session_id": session_id}) + await session.send_and_wait({"prompt": initial_message}, timeout=30.0) + await session.destroy() + print(f" βœ“ Created and saved: {session_id}") + + # List all available sessions + print("\nListing all sessions:") + all_sessions = await client.list_sessions() + + for s in all_sessions: + session_id = s["sessionId"] + modified = s.get("modifiedTime", "Unknown") + summary = s.get("summary", "No summary")[:50] + print(f" - {session_id}") + print(f" Modified: {modified}") + print(f" Summary: {summary}...") + + print(f"\nTotal sessions: {len(all_sessions)}") + + # Resume a specific session + print("\nResuming 'user-alice-support'...") + try: + alice_session = await client.resume_session("user-alice-support") + + # Get the full message history + history = await alice_session.get_messages() + print(f" βœ“ Restored with {len(history)} events") + + # Show event types in history + event_types = {} + for event in history: + event_type = event.type + event_types[event_type] = event_types.get(event_type, 0) + 1 + + print(" Event breakdown:") + for event_type, count in sorted(event_types.items()): + print(f" - {event_type}: {count}") + + await alice_session.destroy() + + except RuntimeError as e: + print(f" βœ— Could not resume: {e}") + + # Clean up all demo sessions + print("\nCleaning up demo sessions...") + for session_id, _ in sessions_to_create: + try: + await client.delete_session(session_id) + print(f" βœ“ Deleted: {session_id}") + except RuntimeError: + print(f" ⚠ Already deleted: {session_id}") + + finally: + await client.stop() + + +# ============================================================================= +# Infinite Sessions with Compaction +# ============================================================================= + + +async def infinite_sessions_demo(): + """Use infinite sessions with automatic context compaction.""" + print("\n=== Infinite Sessions with Compaction ===\n") + + client = CopilotClient() + + try: + await client.start() + + # Create a session with infinite session configuration + session = await client.create_session( + { + "session_id": "infinite-demo", + "infinite_sessions": { + "enabled": True, + "background_compaction_threshold": 0.80, + "buffer_exhaustion_threshold": 0.95, + }, + } + ) + + print(f"βœ“ Created infinite session: {session.session_id}") + + # Check workspace path (where session state is stored) + if session.workspace_path: + print(f" Workspace: {session.workspace_path}") + + # Send some messages to build up context + print("\nBuilding conversation context...") + + topics = [ + "Tell me about Python's GIL in 2 sentences.", + "Explain async/await in 2 sentences.", + "What are decorators? 2 sentences.", + ] + + for topic in topics: + await session.send_and_wait({"prompt": topic}, timeout=60.0) + print(f" βœ“ Discussed: {topic[:30]}...") + + # Get message count + messages = await session.get_messages() + print(f"\nβœ“ Session has {len(messages)} events") + + # When the context window fills up, compaction happens automatically + # The session remains usable without losing important context + + await session.destroy() + + # Resume to verify persistence + print("\nResuming infinite session...") + resumed = await client.resume_session("infinite-demo") + + messages = await resumed.get_messages() + print(f"βœ“ Restored with {len(messages)} events") + + await resumed.destroy() + + # Cleanup + await client.delete_session("infinite-demo") + print("βœ“ Cleaned up infinite session") + + finally: + await client.stop() + + +# ============================================================================= +# Session Export Pattern +# ============================================================================= + + +async def session_export_pattern(): + """Export and inspect session history.""" + print("\n=== Session Export Pattern ===\n") + + client = CopilotClient() + + try: + await client.start() + + # Create a session with some conversation + session = await client.create_session({"session_id": "export-demo"}) + + # Have a short conversation + exchanges = [ + "What is the Fibonacci sequence?", + "Show me the first 10 Fibonacci numbers.", + "What's the 50th Fibonacci number?", + ] + + for prompt in exchanges: + await session.send_and_wait({"prompt": prompt}, timeout=60.0) + + # Export the full history + history = await session.get_messages() + + print(f"Exported {len(history)} events from session\n") + + # Analyze the conversation + print("Conversation summary:") + for event in history: + event_type = event.type + + if event_type == "user.message": + content = getattr(event.data, "content", "") + print(f" πŸ‘€ User: {content[:60]}...") + + elif event_type == "assistant.message": + content = getattr(event.data, "content", "") + # Truncate long responses + display = content[:100] + "..." if len(content) > 100 else content + print(f" πŸ€– Assistant: {display}") + + elif event_type == "tool.execution_complete": + tool_name = getattr(event.data, "tool_name", "unknown") + print(f" βš™οΈ Tool: {tool_name}") + + await session.destroy() + await client.delete_session("export-demo") + print("\nβœ“ Session exported and cleaned up") + + finally: + await client.stop() + + +# ============================================================================= +# Conversation Bookmarks +# ============================================================================= + + +async def conversation_bookmarks(): + """Save conversation checkpoints for later resumption.""" + print("\n=== Conversation Bookmarks Pattern ===\n") + + client = CopilotClient() + + try: + await client.start() + + # Simulate a user's multi-part task + base_id = f"user123-task-{datetime.now().strftime('%Y%m%d')}" + + # Checkpoint 1: Initial planning + session = await client.create_session({"session_id": f"{base_id}-planning"}) + await session.send_and_wait( + {"prompt": "I want to build a web scraper. What are the steps?"}, + timeout=60.0, + ) + await session.destroy() + print("βœ“ Saved checkpoint: planning") + + # Checkpoint 2: Implementation + session = await client.create_session( + {"session_id": f"{base_id}-implementation"} + ) + await session.send_and_wait( + {"prompt": "Show me Python code for a basic web scraper."}, + timeout=60.0, + ) + await session.destroy() + print("βœ“ Saved checkpoint: implementation") + + # List bookmarks for this task + all_sessions = await client.list_sessions() + task_sessions = [s for s in all_sessions if base_id in s["sessionId"]] + + print(f"\nBookmarks for task {base_id}:") + for s in task_sessions: + print(f" - {s['sessionId']}") + print(f" Modified: {s.get('modifiedTime', 'Unknown')}") + + # User can resume any checkpoint + print("\nResuming 'planning' checkpoint...") + planning = await client.resume_session(f"{base_id}-planning") + messages = await planning.get_messages() + print(f"βœ“ Restored planning session with {len(messages)} events") + await planning.destroy() + + # Cleanup + for s in task_sessions: + await client.delete_session(s["sessionId"]) + print("\nβœ“ Cleaned up all checkpoints") -client = CopilotClient() -client.start() + finally: + await client.stop() -# Create session with a memorable ID -session = client.create_session( - session_id="user-123-conversation", - model="gpt-5", -) -session.send(prompt="Let's discuss TypeScript generics") -print(f"Session created: {session.session_id}") +# ============================================================================= +# Main +# ============================================================================= -# Destroy session but keep data on disk -session.destroy() -print("Session destroyed (state persisted)") -# Resume the previous session -resumed = client.resume_session("user-123-conversation") -print(f"Resumed: {resumed.session_id}") +async def main(): + """Run all session persistence demonstrations.""" + print("=" * 60) + print("Session Persistence Patterns") + print("=" * 60) -resumed.send(prompt="What were we discussing?") + await basic_persistence() + await session_management() + await infinite_sessions_demo() + await session_export_pattern() + await conversation_bookmarks() -# List sessions -sessions = client.list_sessions() -print("Sessions:", [s["sessionId"] for s in sessions]) + print("\n" + "=" * 60) + print("All patterns demonstrated!") + print("=" * 60) -# Delete session permanently -client.delete_session("user-123-conversation") -print("Session deleted") -resumed.destroy() -client.stop() +if __name__ == "__main__": + asyncio.run(main()) diff --git a/cookbook/python/recipe/pr_visualization.py b/cookbook/python/recipe/pr_visualization.py index 72226c3d..3110ed11 100644 --- a/cookbook/python/recipe/pr_visualization.py +++ b/cookbook/python/recipe/pr_visualization.py @@ -1,85 +1,223 @@ #!/usr/bin/env python3 +""" +PR Age Chart Generator - Visualizes pull request age distribution. +Run: python pr_visualization.py [--repo owner/repo] [--output path] +""" -import subprocess -import sys +import asyncio import os import re +import subprocess +import sys + from copilot import CopilotClient +from copilot.types import SessionEventType + -# ============================================================================ +# ============================================================================= # Git & GitHub Detection -# ============================================================================ +# ============================================================================= + def is_git_repo(): + """Check if current directory is inside a Git repository.""" try: - subprocess.run( - ["git", "rev-parse", "--git-dir"], - check=True, - capture_output=True - ) + subprocess.run(["git", "rev-parse", "--git-dir"], check=True, capture_output=True) return True except (subprocess.CalledProcessError, FileNotFoundError): return False + def get_github_remote(): + """Extract the GitHub owner/repo from git remote URL.""" try: result = subprocess.run( ["git", "remote", "get-url", "origin"], - check=True, - capture_output=True, - text=True + check=True, capture_output=True, text=True, ) remote_url = result.stdout.strip() - # Handle SSH: git@github.com:owner/repo.git - ssh_match = re.search(r"git@github\.com:(.+/.+?)(?:\.git)?$", remote_url) - if ssh_match: - return ssh_match.group(1) + if match := re.search(r"git@github\.com:(.+/.+?)(?:\.git)?$", remote_url): + return match[1] + if match := re.search(r"https://github\.com/(.+/.+?)(?:\.git)?$", remote_url): + return match[1] + return None + except (subprocess.CalledProcessError, FileNotFoundError): + return None - # Handle HTTPS: https://github.com/owner/repo.git - https_match = re.search(r"https://github\.com/(.+/.+?)(?:\.git)?$", remote_url) - if https_match: - return https_match.group(1) - return None +def get_git_branch(): + """Get current git branch name.""" + try: + result = subprocess.run( + ["git", "branch", "--show-current"], + check=True, capture_output=True, text=True, + ) + return result.stdout.strip() except (subprocess.CalledProcessError, FileNotFoundError): return None + def parse_args(): + """Parse command line arguments.""" args = sys.argv[1:] + result = {} + if "--repo" in args: idx = args.index("--repo") if idx + 1 < len(args): - return {"repo": args[idx + 1]} - return {} + result["repo"] = args[idx + 1] + + if "--output" in args: + idx = args.index("--output") + if idx + 1 < len(args): + result["output"] = args[idx + 1] + + if "--help" in args or "-h" in args: + result["help"] = "true" + + return result + + +def print_help(): + """Print usage information.""" + print(""" +PR Age Chart Generator + +Usage: python pr_visualization.py [options] + +Options: + --repo OWNER/REPO GitHub repository (e.g., github/copilot-sdk) + --output PATH Output path for chart (default: pr-age-chart.png) + --help Show this help +""") + def prompt_for_repo(): + """Prompt user for a repository.""" return input("Enter GitHub repo (owner/repo): ").strip() -# ============================================================================ -# Main Application -# ============================================================================ -def main(): - print("πŸ” PR Age Chart Generator\n") +# ============================================================================= +# Event Handler +# ============================================================================= + + +def create_event_handler(verbose=True): + """Create an event handler for displaying progress.""" + + def handle_event(event): + if event.type == SessionEventType.ASSISTANT_MESSAGE: + print(f"\nπŸ€– {event.data.content}\n") + elif event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA and verbose: + delta = getattr(event.data, "delta_content", "") + if delta: + print(delta, end="", flush=True) + elif event.type == SessionEventType.TOOL_EXECUTION_START: + print(f" βš™οΈ {event.data.tool_name}") + elif event.type == SessionEventType.TOOL_EXECUTION_COMPLETE: + print(" βœ“ Done") + elif event.type == SessionEventType.SESSION_ERROR: + message = getattr(event.data, "message", str(event.data)) + print(f" βœ— Error: {message}") + + return handle_event + + +# ============================================================================= +# PR Analysis Functions +# ============================================================================= + + +async def analyze_pr_age(session, owner, repo_name, output_path): + """Analyze PR age distribution and generate a chart.""" + prompt = f""" +Fetch open pull requests for {owner}/{repo_name}. + +Calculate age and last activity for each PR. +Generate a bar chart grouping by age (<1 day, 1-3 days, 3-7 days, 1-2 weeks, 2-4 weeks, >1 month). +Save as "{output_path}". +Summarize: total open PRs, average/median age, oldest PR, stale PRs (>2 weeks). +""" + await session.send_and_wait({"prompt": prompt}, timeout=300.0) + + +async def analyze_pr_by_author(session, owner, repo_name): + """Analyze PRs grouped by author.""" + prompt = f""" +Fetch open pull requests for {owner}/{repo_name}. +Group by author, show: count per author, average age, stale PRs. +Generate a horizontal bar chart and save as "pr-by-author.png". +""" + await session.send_and_wait({"prompt": prompt}, timeout=300.0) + + +async def analyze_pr_review_status(session, owner, repo_name): + """Analyze PR review status.""" + prompt = f""" +Fetch open pull requests for {owner}/{repo_name}. +Analyze review status: waiting for review, changes requested, approved but not merged. +Identify bottlenecks. Generate a pie chart as "pr-review-status.png". +""" + await session.send_and_wait({"prompt": prompt}, timeout=300.0) + + +# ============================================================================= +# Interactive Loop +# ============================================================================= + + +async def interactive_loop(session): + """Run interactive follow-up loop.""" + print("\n" + "-" * 50) + print("πŸ’‘ Ask follow-up questions or 'exit' to quit.") + print("Examples: 'Show oldest PRs', 'Group by author', 'Generate pie chart'\n") + + while True: + try: + user_input = input("You: ").strip() + except (EOFError, KeyboardInterrupt): + print("\nπŸ‘‹ Goodbye!") + break + + if user_input.lower() in ("exit", "quit", "q"): + break + + if user_input: + await session.send_and_wait({"prompt": user_input}, timeout=300.0) + + +# ============================================================================= +# Main +# ============================================================================= + + +async def main(): + """Main entry point for PR Age Chart Generator.""" + print("=" * 60) + print("πŸ” PR Age Chart Generator") + print("=" * 60) - # Determine the repository args = parse_args() - repo = None + if args.get("help"): + print_help() + return + + # Determine repository + repo = None if "repo" in args: repo = args["repo"] - print(f"πŸ“¦ Using specified repo: {repo}") + print(f"\nπŸ“¦ Using: {repo}") elif is_git_repo(): detected = get_github_remote() if detected: repo = detected - print(f"πŸ“¦ Detected GitHub repo: {repo}") + branch = get_git_branch() + print(f"\nπŸ“¦ Detected: {repo}" + (f" ({branch})" if branch else "")) else: - print("⚠️ Git repo found but no GitHub remote detected.") repo = prompt_for_repo() else: - print("πŸ“ Not in a git repository.") repo = prompt_for_repo() if not repo or "/" not in repo: @@ -87,75 +225,41 @@ def main(): sys.exit(1) owner, repo_name = repo.split("/", 1) + output_path = args.get("output", "pr-age-chart.png") - # Create Copilot client - no custom tools needed! - client = CopilotClient(log_level="error") - client.start() + client = CopilotClient({"log_level": "error"}) - session = client.create_session( - model="gpt-5", - system_message={ - "content": f""" + try: + await client.start() + print("βœ“ Connected to Copilot") + + session = await client.create_session({ + "system_message": { + "mode": "append", + "content": f""" -You are analyzing pull requests for the GitHub repository: {owner}/{repo_name} -The current working directory is: {os.getcwd()} +Analyzing PRs for: {owner}/{repo_name} +Working directory: {os.getcwd()} +Output path: {output_path} +""", + }, + }) - -- Use the GitHub MCP Server tools to fetch PR data -- Use your file and code execution tools to generate charts -- Save any generated images to the current working directory -- Be concise in your responses - -""" - } - ) + session.on(create_event_handler(verbose=False)) - # Set up event handling - def handle_event(event): - if event["type"] == "assistant.message": - print(f"\nπŸ€– {event['data']['content']}\n") - elif event["type"] == "tool.execution_start": - print(f" βš™οΈ {event['data']['toolName']}") - - session.on(handle_event) - - # Initial prompt - let Copilot figure out the details - print("\nπŸ“Š Starting analysis...\n") - - session.send(prompt=f""" - Fetch the open pull requests for {owner}/{repo_name} from the last week. - Calculate the age of each PR in days. - Then generate a bar chart image showing the distribution of PR ages - (group them into sensible buckets like <1 day, 1-3 days, etc.). - Save the chart as "pr-age-chart.png" in the current directory. - Finally, summarize the PR health - average age, oldest PR, and how many might be considered stale. - """) - - session.wait_for_idle() - - # Interactive loop - print("\nπŸ’‘ Ask follow-up questions or type \"exit\" to quit.\n") - print("Examples:") - print(" - \"Expand to the last month\"") - print(" - \"Show me the 5 oldest PRs\"") - print(" - \"Generate a pie chart instead\"") - print(" - \"Group by author instead of age\"") - print() + print("\nπŸ“Š Starting PR analysis...\n") + await analyze_pr_age(session, owner, repo_name, output_path) + await interactive_loop(session) + await session.destroy() - while True: - user_input = input("You: ").strip() - - if user_input.lower() in ["exit", "quit"]: - print("πŸ‘‹ Goodbye!") - break + except Exception as e: + print(f"\nβœ— Error: {e}") + sys.exit(1) - if user_input: - session.send(prompt=user_input) - session.wait_for_idle() + finally: + await client.stop() - session.destroy() - client.stop() if __name__ == "__main__": - main() + asyncio.run(main()) diff --git a/cookbook/python/recipe/requirements.txt b/cookbook/python/recipe/requirements.txt index 91d70ef1..b3dad4b1 100644 --- a/cookbook/python/recipe/requirements.txt +++ b/cookbook/python/recipe/requirements.txt @@ -1,2 +1,2 @@ # Install the local Copilot SDK package in editable mode --e ../.. +-e ../../../python diff --git a/cookbook/python/recipe/streaming_responses.py b/cookbook/python/recipe/streaming_responses.py new file mode 100644 index 00000000..af439ea4 --- /dev/null +++ b/cookbook/python/recipe/streaming_responses.py @@ -0,0 +1,252 @@ +#!/usr/bin/env python3 +""" +Streaming Responses - Real-time streaming with the Copilot SDK. + +Run: python streaming_responses.py +""" + +import asyncio +import sys + +from copilot import CopilotClient +from copilot.types import SessionEventType + + +# ============================================================================= +# Basic Streaming +# ============================================================================= + + +async def basic_streaming(): + """Stream responses in real-time.""" + print("\n=== Basic Streaming ===\n") + + client = CopilotClient() + + try: + await client.start() + session = await client.create_session({"streaming": True}) + + print("Ask: Write a haiku about programming\n") + print("Response: ", end="", flush=True) + + def handler(event): + if event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA: + delta = getattr(event.data, "delta_content", "") + if delta: + print(delta, end="", flush=True) + elif event.type == SessionEventType.SESSION_IDLE: + print("\n") + + session.on(handler) + + await session.send_and_wait( + {"prompt": "Write a haiku about programming"}, + timeout=60.0 + ) + + await session.destroy() + + finally: + await client.stop() + + +# ============================================================================= +# Streaming with Progress +# ============================================================================= + + +async def streaming_with_progress(): + """Show progress indicators during streaming.""" + print("\n=== Streaming with Progress ===\n") + + client = CopilotClient() + + try: + await client.start() + session = await client.create_session({"streaming": True}) + + state = {"thinking": False, "tools": 0, "chars": 0} + + def handler(event): + if event.type == SessionEventType.ASSISTANT_REASONING_DELTA: + if not state["thinking"]: + print("\nπŸ’­ Thinking: ", end="", flush=True) + state["thinking"] = True + delta = getattr(event.data, "delta_content", "") + if delta: + print(delta, end="", flush=True) + + elif event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA: + if state["thinking"]: + print("\n\nπŸ“ Response: ", end="", flush=True) + state["thinking"] = False + delta = getattr(event.data, "delta_content", "") + if delta: + print(delta, end="", flush=True) + state["chars"] += len(delta) + + elif event.type == SessionEventType.TOOL_EXECUTION_START: + state["tools"] += 1 + print(f"\n πŸ”§ [{state['tools']}] {event.data.tool_name}...", end="") + + elif event.type == SessionEventType.TOOL_EXECUTION_COMPLETE: + print(" βœ“") + + elif event.type == SessionEventType.SESSION_IDLE: + print(f"\n\nπŸ“Š {state['chars']} chars, {state['tools']} tools") + + session.on(handler) + + print("Ask: Explain recursion with a code example\n") + + await session.send_and_wait( + {"prompt": "Explain recursion with a simple Python example"}, + timeout=120.0 + ) + + await session.destroy() + + finally: + await client.stop() + + +# ============================================================================= +# Interactive Chat +# ============================================================================= + + +async def interactive_chat(): + """Interactive chat with streaming.""" + print("\n=== Interactive Chat ===\n") + print("Type messages. Press Ctrl+C or 'exit' to quit.\n") + + client = CopilotClient({"log_level": "error"}) + + try: + await client.start() + session = await client.create_session({"streaming": True}) + + response_started = False + + def handler(event): + nonlocal response_started + + if event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA: + if not response_started: + print("\nπŸ€– ", end="", flush=True) + response_started = True + delta = getattr(event.data, "delta_content", "") + if delta: + print(delta, end="", flush=True) + + elif event.type == SessionEventType.TOOL_EXECUTION_START: + print(f"\n βš™οΈ {event.data.tool_name}", end="") + + elif event.type == SessionEventType.SESSION_IDLE: + if response_started: + print("\n") + response_started = False + + session.on(handler) + + while True: + try: + user_input = input("You: ").strip() + except (EOFError, KeyboardInterrupt): + print("\nπŸ‘‹ Goodbye!") + break + + if user_input.lower() in ["exit", "quit"]: + print("πŸ‘‹ Goodbye!") + break + + if user_input: + response_started = False + await session.send_and_wait({"prompt": user_input}, timeout=120.0) + + await session.destroy() + + finally: + await client.stop() + + +# ============================================================================= +# Typewriter Effect +# ============================================================================= + + +async def typewriter_effect(): + """Display with typewriter animation.""" + print("\n=== Typewriter Effect ===\n") + + client = CopilotClient() + + try: + await client.start() + session = await client.create_session({"streaming": True}) + + buffer = [] + complete = asyncio.Event() + + def handler(event): + if event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA: + delta = getattr(event.data, "delta_content", "") + if delta: + buffer.append(delta) + elif event.type == SessionEventType.SESSION_IDLE: + complete.set() + + session.on(handler) + + # Start request + asyncio.create_task(session.send_and_wait( + {"prompt": "Write a short poem about code."}, + timeout=60.0 + )) + + print("Response: ", end="", flush=True) + + # Display with delay + while not complete.is_set() or buffer: + if buffer: + chunk = buffer.pop(0) + for char in chunk: + print(char, end="", flush=True) + await asyncio.sleep(0.02) + else: + await asyncio.sleep(0.05) + + print("\n") + await session.destroy() + + finally: + await client.stop() + + +# ============================================================================= +# Main +# ============================================================================= + + +async def main(): + print("=" * 50) + print("STREAMING RESPONSES") + print("=" * 50) + + await basic_streaming() + await streaming_with_progress() + await typewriter_effect() + + # Skip interactive in automated runs + if sys.stdin.isatty(): + run_interactive = input("\nRun interactive chat? (y/n): ").lower() + if run_interactive == "y": + await interactive_chat() + + print("\n" + "=" * 50) + print("All demos completed!") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/cookbook/python/streaming-responses.md b/cookbook/python/streaming-responses.md new file mode 100644 index 00000000..81aff677 --- /dev/null +++ b/cookbook/python/streaming-responses.md @@ -0,0 +1,349 @@ +# Streaming Responses + +Handle real-time streaming for progressive output and better user experience. + +> **Skill Level:** Intermediate +> +> **Runnable Example:** [recipe/streaming_responses.py](recipe/streaming_responses.py) +> +> ```bash +> cd recipe && pip install -r requirements.txt +> python streaming_responses.py +> ``` + +## Overview + +This recipe covers streaming patterns: + +- Basic streaming with `send()` and events +- Progress indicators during generation +- Typewriter effect for chat UIs +- Parallel streaming from multiple sessions +- Chunk processing and aggregation + +## Quick Start + +```python +import asyncio +from copilot import CopilotClient +from copilot.types import SessionEventType + +async def main(): + client = CopilotClient() + await client.start() + + session = await client.create_session() + + # Stream handler for real-time output + def on_stream(event): + if event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA: + # Print each chunk as it arrives + print(event.data.delta_content, end="", flush=True) + elif event.type == SessionEventType.ASSISTANT_MESSAGE: + print() # Newline after complete message + + session.on(on_stream) + + # send() returns immediately, events stream in + await session.send({"prompt": "Write a haiku about Python programming"}) + + # Wait for completion + await asyncio.sleep(5) + + await session.destroy() + await client.stop() + +asyncio.run(main()) +``` + +## Streaming Events + +| Event Type | SessionEventType | Description | +|------------|------------------|-------------| +| `assistant.message_delta` | `ASSISTANT_MESSAGE_DELTA` | Partial content chunk | +| `assistant.message` | `ASSISTANT_MESSAGE` | Complete message | +| `tool.execution_start` | `TOOL_EXECUTION_START` | Tool starting | +| `tool.execution_complete` | `TOOL_EXECUTION_COMPLETE` | Tool finished | +| `session.idle` | `SESSION_IDLE` | Session idle | + +## Basic Streaming + +Stream responses with full event handling: + +```python +async def stream_response(session, prompt): + """Stream a response with progress tracking.""" + chunks = [] + complete = asyncio.Event() + + def handler(event): + if event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA: + delta = getattr(event.data, "delta_content", "") + chunks.append(delta) + print(delta, end="", flush=True) + + elif event.type == SessionEventType.ASSISTANT_MESSAGE: + print() # Newline + complete.set() + + elif event.type == SessionEventType.SESSION_ERROR: + print(f"\nError: {event.data.message}") + complete.set() + + session.on(handler) + await session.send({"prompt": prompt}) + await complete.wait() + + return "".join(chunks) +``` + +## Progress Indicators + +Show progress during generation: + +```python +async def stream_with_progress(session, prompt): + """Stream with visual progress indicator.""" + import sys + + chunk_count = 0 + spinner = ['β ‹', 'β ™', 'β Ή', 'β Έ', 'β Ό', 'β ΄', 'β ¦', 'β §', 'β ‡', '⠏'] + + def handler(event): + nonlocal chunk_count + + if event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA: + chunk_count += 1 + # Show spinner + sys.stdout.write(f"\r{spinner[chunk_count % len(spinner)]} Generating...") + sys.stdout.flush() + + elif event.type == SessionEventType.ASSISTANT_MESSAGE: + sys.stdout.write(f"\rβœ… Complete ({chunk_count} chunks)\n\n") + print(event.data.content) + + elif event.type == SessionEventType.TOOL_EXECUTION_START: + print(f"\nπŸ”§ Running {event.data.tool_name}...") + + session.on(handler) + await session.send_and_wait({"prompt": prompt}) +``` + +## Typewriter Effect + +Create a chat-like typing experience: + +```python +async def typewriter_effect(session, prompt, delay=0.02): + """Display response with typewriter effect.""" + complete = asyncio.Event() + + async def display_chunk(text): + for char in text: + print(char, end="", flush=True) + await asyncio.sleep(delay) + + buffer = [] + display_task = None + + def handler(event): + nonlocal display_task + + if event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA: + delta = getattr(event.data, "delta_content", "") + buffer.append(delta) + + elif event.type == SessionEventType.ASSISTANT_MESSAGE: + complete.set() + + session.on(handler) + await session.send({"prompt": prompt}) + + # Display buffered content with delay + while not complete.is_set() or buffer: + if buffer: + chunk = buffer.pop(0) + await display_chunk(chunk) + else: + await asyncio.sleep(0.01) + + print() # Final newline +``` + +## Parallel Streaming + +Stream from multiple sessions simultaneously: + +```python +async def parallel_streaming(): + """Stream responses from multiple sessions in parallel.""" + client = CopilotClient() + await client.start() + + topics = ["Python", "JavaScript", "Rust"] + sessions = [] + results = {topic: [] for topic in topics} + events = {topic: asyncio.Event() for topic in topics} + + # Create sessions with handlers + for topic in topics: + session = await client.create_session() + + def make_handler(t): + def handler(event): + if event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA: + delta = getattr(event.data, "delta_content", "") + results[t].append(delta) + elif event.type == SessionEventType.ASSISTANT_MESSAGE: + events[t].set() + return handler + + session.on(make_handler(topic)) + sessions.append((topic, session)) + + # Send all prompts + for topic, session in sessions: + await session.send({"prompt": f"Describe {topic} in 2 sentences"}) + + # Wait for all to complete + await asyncio.gather(*[e.wait() for e in events.values()]) + + # Print results + for topic in topics: + print(f"\n{topic}: {''.join(results[topic])}") + + # Cleanup + for _, session in sessions: + await session.destroy() + await client.stop() +``` + +## Stream Aggregation + +Collect and process streamed content: + +```python +class StreamAggregator: + """Aggregate streaming content for processing.""" + + def __init__(self): + self.chunks = [] + self.complete = asyncio.Event() + self.tool_calls = [] + + def handler(self, event): + if event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA: + delta = getattr(event.data, "delta_content", "") + self.chunks.append(delta) + + elif event.type == SessionEventType.ASSISTANT_MESSAGE: + self.complete.set() + + elif event.type == SessionEventType.TOOL_EXECUTION_COMPLETE: + self.tool_calls.append({ + "id": event.data.tool_call_id, + "result": event.data.result + }) + + @property + def content(self): + return "".join(self.chunks) + + async def wait(self, timeout=60.0): + await asyncio.wait_for(self.complete.wait(), timeout) + + +# Usage +aggregator = StreamAggregator() +session.on(aggregator.handler) +await session.send({"prompt": "Analyze this code..."}) +await aggregator.wait() +print(f"Response: {aggregator.content}") +print(f"Tools called: {len(aggregator.tool_calls)}") +``` + +## Rich Console Output + +Use rich library for enhanced display: + +```python +from rich.console import Console +from rich.live import Live +from rich.markdown import Markdown + +async def rich_streaming(session, prompt): + """Stream with rich formatting.""" + console = Console() + content = [] + + def handler(event): + if event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA: + delta = getattr(event.data, "delta_content", "") + content.append(delta) + + session.on(handler) + + with Live(console=console, refresh_per_second=10) as live: + await session.send({"prompt": prompt}) + + while True: + # Update display with markdown + live.update(Markdown("".join(content))) + await asyncio.sleep(0.1) + + if session.is_idle: + break +``` + +## Timeout Handling + +Handle slow or stalled streams: + +```python +async def stream_with_timeout(session, prompt, timeout=30.0): + """Stream with timeout protection.""" + complete = asyncio.Event() + last_chunk_time = asyncio.get_event_loop().time() + + def handler(event): + nonlocal last_chunk_time + last_chunk_time = asyncio.get_event_loop().time() + + if event.type == SessionEventType.ASSISTANT_MESSAGE: + complete.set() + + session.on(handler) + await session.send({"prompt": prompt}) + + try: + await asyncio.wait_for(complete.wait(), timeout=timeout) + except asyncio.TimeoutError: + print(f"Stream timed out after {timeout}s") + await session.abort() # Cancel the generation +``` + +## Best Practices + +1. **Use `send()` for streaming**: Returns immediately, events stream in +2. **Handle all event types**: Don't just handle deltas, handle errors too +3. **Buffer appropriately**: Don't overwhelm the display +4. **Set timeouts**: Protect against stalled streams +5. **Clean up handlers**: Remove handlers when done if reusing sessions + +## Complete Example + +```bash +python recipe/streaming_responses.py +``` + +Demonstrates: +- Basic streaming +- Progress indicators +- Typewriter effect +- Parallel streaming + +## Next Steps + +- [Error Handling](error-handling.md): Handle streaming errors +- [Custom Tools](custom-tools.md): Stream tool results +- [Multiple Sessions](multiple-sessions.md): Parallel streaming patterns diff --git a/docs/custom-agents.md b/docs/custom-agents.md new file mode 100644 index 00000000..cdc3eb6d --- /dev/null +++ b/docs/custom-agents.md @@ -0,0 +1,805 @@ +# Custom Agents + +Custom agents are specialized AI assistants with custom prompts, tools, and behaviors. They allow you to create domain-specific assistants that excel at particular tasks like code review, SQL optimization, or documentation writing. + +## What is an Agent? + +An agent is a session configuration that defines: + +- **Custom system prompt**: Specialized behavior and expertise +- **Specific tools**: Limited or extended capabilities +- **Focused context**: Domain-specific knowledge + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Custom Agent β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Name: "code-reviewer" β”‚ +β”‚ Prompt: "You are an expert code reviewer" β”‚ +β”‚ Tools: [analyze_code, check_style] β”‚ +β”‚ Infer: true (auto-selection enabled) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## Quick Start + +
+Node.js / TypeScript + +```typescript +import { CopilotClient, SessionEventType } from "@github/copilot-sdk"; + +const client = new CopilotClient(); +const session = await client.createSession({ + model: "gpt-4.1", + customAgents: [ + { + name: "code-reviewer", + displayName: "Code Review Expert", + description: "Reviews code for bugs, security, and best practices", + prompt: `You are an expert code reviewer specializing in: +- Code quality and readability +- Security vulnerabilities (OWASP Top 10) +- Performance optimization +- Best practices and design patterns + +When reviewing, start with a summary, then list issues by severity.`, + tools: null, // Use all available tools + infer: true, // Enable automatic selection + }, + ], +}); + +session.on((event) => { + if (event.type === SessionEventType.AssistantMessage) { + console.log(event.data.content); + } +}); + +await session.sendAndWait({ + prompt: "@code-reviewer Review this function:\n\ndef get_user(id): return db.query(f'SELECT * FROM users WHERE id={id}')" +}); + +await client.stop(); +``` + +
+ +
+Python + +```python +import asyncio +from copilot import CopilotClient +from copilot.types import CustomAgentConfig, SessionEventType + +async def main(): + client = CopilotClient() + await client.start() + + code_reviewer = CustomAgentConfig( + name="code-reviewer", + display_name="Code Review Expert", + description="Reviews code for bugs, security, and best practices", + prompt="""You are an expert code reviewer specializing in: +- Code quality and readability +- Security vulnerabilities (OWASP Top 10) +- Performance optimization +- Best practices and design patterns + +When reviewing, start with a summary, then list issues by severity.""", + tools=None, # Use all available tools + infer=True, # Enable automatic selection + ) + + session = await client.create_session({ + "model": "gpt-4.1", + "custom_agents": [code_reviewer], + }) + + def handle_event(event): + if event.type == SessionEventType.ASSISTANT_MESSAGE: + print(event.data.content) + + session.on(handle_event) + + await session.send_and_wait({ + "prompt": "@code-reviewer Review this function:\n\ndef get_user(id): return db.query(f'SELECT * FROM users WHERE id={id}')" + }) + + await client.stop() + +asyncio.run(main()) +``` + +
+ +
+Go + +```go +package main + +import ( + "fmt" + "log" + + copilot "github.com/github/copilot-sdk/go" +) + +func main() { + client := copilot.NewClient(nil) + if err := client.Start(); err != nil { + log.Fatal(err) + } + defer client.Stop() + + session, err := client.CreateSession(&copilot.SessionConfig{ + Model: "gpt-4.1", + CustomAgents: []copilot.CustomAgentConfig{ + { + Name: "code-reviewer", + DisplayName: "Code Review Expert", + Description: "Reviews code for bugs, security, and best practices", + Prompt: `You are an expert code reviewer specializing in: +- Code quality and readability +- Security vulnerabilities (OWASP Top 10) +- Performance optimization +- Best practices and design patterns + +When reviewing, start with a summary, then list issues by severity.`, + Tools: nil, // Use all available tools + Infer: true, // Enable automatic selection + }, + }, + }) + if err != nil { + log.Fatal(err) + } + + session.On(func(event copilot.SessionEvent) { + if event.Type == copilot.SessionEventTypeAssistantMessage { + fmt.Println(*event.Data.Content) + } + }) + + _, err = session.SendAndWait(copilot.MessageOptions{ + Prompt: "@code-reviewer Review this function:\n\ndef get_user(id): return db.query(f'SELECT * FROM users WHERE id={id}')", + }, 0) + if err != nil { + log.Fatal(err) + } +} +``` + +
+ +
+.NET + +```csharp +using GitHub.Copilot.SDK; + +await using var client = new CopilotClient(); +await using var session = await client.CreateSessionAsync(new SessionConfig +{ + Model = "gpt-4.1", + CustomAgents = new[] + { + new CustomAgentConfig + { + Name = "code-reviewer", + DisplayName = "Code Review Expert", + Description = "Reviews code for bugs, security, and best practices", + Prompt = @"You are an expert code reviewer specializing in: +- Code quality and readability +- Security vulnerabilities (OWASP Top 10) +- Performance optimization +- Best practices and design patterns + +When reviewing, start with a summary, then list issues by severity.", + Tools = null, // Use all available tools + Infer = true, // Enable automatic selection + }, + }, +}); + +session.On(e => +{ + if (e.Type == SessionEventType.AssistantMessage) + { + Console.WriteLine(e.Data.Content); + } +}); + +await session.SendAndWaitAsync(new MessageOptions +{ + Prompt = "@code-reviewer Review this function:\n\ndef get_user(id): return db.query(f'SELECT * FROM users WHERE id={id}')" +}); +``` + +
+ +## Agent Configuration Options + +| Property | Type | Required | Description | +|----------|------|----------|-------------| +| `name` | `string` | Yes | Unique identifier for the agent (used with `@` mentions) | +| `display_name` | `string` | No | Human-readable name for UI display | +| `description` | `string` | No | Description of what the agent does (helps with auto-selection) | +| `prompt` | `string` | Yes | System prompt defining the agent's behavior and expertise | +| `tools` | `string[]` or `null` | No | List of allowed tools (`null` = all, `[]` = none) | +| `mcp_servers` | `object` | No | MCP servers specific to this agent | +| `infer` | `boolean` | No | Whether the agent can be auto-selected based on context | + +## Using Agents + +### Explicit Selection with @mention + +Call a specific agent by name: + +``` +@code-reviewer Check this code for security issues +@sql-expert Optimize this query for performance +@doc-writer Write API documentation for this function +``` + +### Automatic Selection (Infer) + +When `infer: true`, Copilot automatically selects the best agent based on the prompt context: + +``` +"Review this Python function for bugs" β†’ Selects code-reviewer +"How do I write a JOIN query?" β†’ Selects sql-expert +"Write a README for this project" β†’ Selects doc-writer +``` + +## Multiple Agents + +Register multiple agents in a single session: + +
+Node.js / TypeScript + +```typescript +const session = await client.createSession({ + customAgents: [ + { + name: "reviewer", + description: "Code review expert", + prompt: "You are an expert code reviewer...", + infer: true, + }, + { + name: "sql-expert", + description: "Database and SQL specialist", + prompt: "You are a database expert...", + infer: true, + }, + { + name: "doc-writer", + description: "Technical documentation writer", + prompt: "You are a technical writer...", + infer: true, + }, + ], +}); + +// Use specific agents +await session.sendAndWait({ prompt: "@reviewer Check this for bugs" }); +await session.sendAndWait({ prompt: "@sql-expert Optimize this query" }); +await session.sendAndWait({ prompt: "@doc-writer Document this API" }); +``` + +
+ +
+Python + +```python +session = await client.create_session({ + "custom_agents": [ + CustomAgentConfig( + name="reviewer", + description="Code review expert", + prompt="You are an expert code reviewer...", + infer=True, + ), + CustomAgentConfig( + name="sql-expert", + description="Database and SQL specialist", + prompt="You are a database expert...", + infer=True, + ), + CustomAgentConfig( + name="doc-writer", + description="Technical documentation writer", + prompt="You are a technical writer...", + infer=True, + ), + ], +}) + +# Use specific agents +await session.send_and_wait({"prompt": "@reviewer Check this for bugs"}) +await session.send_and_wait({"prompt": "@sql-expert Optimize this query"}) +await session.send_and_wait({"prompt": "@doc-writer Document this API"}) +``` + +
+ +
+Go + +```go +session, err := client.CreateSession(&copilot.SessionConfig{ + CustomAgents: []copilot.CustomAgentConfig{ + { + Name: "reviewer", + Description: "Code review expert", + Prompt: "You are an expert code reviewer...", + Infer: true, + }, + { + Name: "sql-expert", + Description: "Database and SQL specialist", + Prompt: "You are a database expert...", + Infer: true, + }, + { + Name: "doc-writer", + Description: "Technical documentation writer", + Prompt: "You are a technical writer...", + Infer: true, + }, + }, +}) +if err != nil { + log.Fatal(err) +} + +// Use specific agents +session.SendAndWait(copilot.MessageOptions{Prompt: "@reviewer Check this for bugs"}, 0) +session.SendAndWait(copilot.MessageOptions{Prompt: "@sql-expert Optimize this query"}, 0) +session.SendAndWait(copilot.MessageOptions{Prompt: "@doc-writer Document this API"}, 0) +``` + +
+ +
+.NET + +```csharp +await using var session = await client.CreateSessionAsync(new SessionConfig +{ + CustomAgents = new[] + { + new CustomAgentConfig + { + Name = "reviewer", + Description = "Code review expert", + Prompt = "You are an expert code reviewer...", + Infer = true, + }, + new CustomAgentConfig + { + Name = "sql-expert", + Description = "Database and SQL specialist", + Prompt = "You are a database expert...", + Infer = true, + }, + new CustomAgentConfig + { + Name = "doc-writer", + Description = "Technical documentation writer", + Prompt = "You are a technical writer...", + Infer = true, + }, + }, +}); + +// Use specific agents +await session.SendAndWaitAsync(new MessageOptions { Prompt = "@reviewer Check this for bugs" }); +await session.SendAndWaitAsync(new MessageOptions { Prompt = "@sql-expert Optimize this query" }); +await session.SendAndWaitAsync(new MessageOptions { Prompt = "@doc-writer Document this API" }); +``` + +
+ +## Agents with Custom Tools + +Combine agents with custom tools for powerful workflows: + +
+Node.js / TypeScript + +```typescript +import { CopilotClient, defineTool } from "@github/copilot-sdk"; +import { z } from "zod"; + +// Define a custom tool +const runLinter = defineTool({ + name: "run_linter", + description: "Run a linter on Python code", + parameters: z.object({ + code: z.string().describe("The code to lint"), + }), + handler: async ({ code }) => { + // In production, actually run pylint/flake8 + return { issues: [], score: 10.0 }; + }, +}); + +const session = await client.createSession({ + tools: [runLinter], + customAgents: [ + { + name: "linter", + description: "Code quality checker with linting", + prompt: "You check code quality. Use the run_linter tool to analyze code.", + tools: ["run_linter"], // Only this tool available + infer: true, + }, + ], +}); +``` + +
+ +
+Python + +```python +from copilot import CopilotClient +from copilot.tools import define_tool +from copilot.types import CustomAgentConfig +from pydantic import BaseModel, Field + +class LintParams(BaseModel): + code: str = Field(description="The code to lint") + +@define_tool(description="Run a linter on Python code") +def run_linter(params): + # In production, actually run pylint/flake8 + return {"issues": [], "score": 10.0} + +session = await client.create_session({ + "tools": [run_linter], + "custom_agents": [ + CustomAgentConfig( + name="linter", + description="Code quality checker with linting", + prompt="You check code quality. Use the run_linter tool to analyze code.", + tools=["run_linter"], # Only this tool available + infer=True, + ), + ], +}) +``` + +
+ +
+Go + +```go +// Define parameter type +type LintParams struct { + Code string `json:"code" jsonschema:"The code to lint"` +} + +// Define the tool +runLinter := copilot.DefineTool( + "run_linter", + "Run a linter on Python code", + func(params LintParams, inv copilot.ToolInvocation) (map[string]interface{}, error) { + // In production, actually run pylint/flake8 + return map[string]interface{}{"issues": []string{}, "score": 10.0}, nil + }, +) + +session, err := client.CreateSession(&copilot.SessionConfig{ + Tools: []copilot.Tool{runLinter}, + CustomAgents: []copilot.CustomAgentConfig{ + { + Name: "linter", + Description: "Code quality checker with linting", + Prompt: "You check code quality. Use the run_linter tool to analyze code.", + Tools: []string{"run_linter"}, // Only this tool available + Infer: true, + }, + }, +}) +``` + +
+ +
+.NET + +```csharp +using Microsoft.Extensions.AI; +using System.ComponentModel; + +// Define a custom tool +var runLinter = AIFunctionFactory.Create( + ([Description("The code to lint")] string code) => + { + // In production, actually run pylint/flake8 + return new { issues = Array.Empty(), score = 10.0 }; + }, + "run_linter", + "Run a linter on Python code" +); + +await using var session = await client.CreateSessionAsync(new SessionConfig +{ + Tools = [runLinter], + CustomAgents = new[] + { + new CustomAgentConfig + { + Name = "linter", + Description = "Code quality checker with linting", + Prompt = "You check code quality. Use the run_linter tool to analyze code.", + Tools = new[] { "run_linter" }, // Only this tool available + Infer = true, + }, + }, +}); +``` + +
+ +## Agents with MCP Servers + +Give agents access to specific MCP servers: + +
+Node.js / TypeScript + +```typescript +import { CopilotClient } from "@github/copilot-sdk"; + +const session = await client.createSession({ + customAgents: [ + { + name: "github-helper", + description: "GitHub operations assistant", + prompt: "You help with GitHub tasks using the GitHub API.", + mcpServers: { + github: { + type: "stdio", + command: "npx", + args: ["-y", "@modelcontextprotocol/server-github"], + env: { GITHUB_TOKEN: process.env.GITHUB_TOKEN }, + tools: ["*"], + }, + }, + infer: true, + }, + ], +}); +``` + +
+ +
+Python + +```python +import os +from copilot import CopilotClient +from copilot.types import CustomAgentConfig + +session = await client.create_session({ + "custom_agents": [ + CustomAgentConfig( + name="github-helper", + description="GitHub operations assistant", + prompt="You help with GitHub tasks using the GitHub API.", + mcp_servers={ + "github": { + "type": "stdio", + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-github"], + "env": {"GITHUB_TOKEN": os.environ["GITHUB_TOKEN"]}, + "tools": ["*"], + }, + }, + infer=True, + ), + ], +}) +``` + +
+ +
+Go + +```go +import "os" + +session, err := client.CreateSession(&copilot.SessionConfig{ + CustomAgents: []copilot.CustomAgentConfig{ + { + Name: "github-helper", + Description: "GitHub operations assistant", + Prompt: "You help with GitHub tasks using the GitHub API.", + MCPServers: map[string]copilot.MCPServerConfig{ + "github": { + Type: "stdio", + Command: "npx", + Args: []string{"-y", "@modelcontextprotocol/server-github"}, + Env: map[string]string{"GITHUB_TOKEN": os.Getenv("GITHUB_TOKEN")}, + Tools: []string{"*"}, + }, + }, + Infer: true, + }, + }, +}) +``` + +
+ +
+.NET + +```csharp +using GitHub.Copilot.SDK; + +await using var session = await client.CreateSessionAsync(new SessionConfig +{ + CustomAgents = new[] + { + new CustomAgentConfig + { + Name = "github-helper", + Description = "GitHub operations assistant", + Prompt = "You help with GitHub tasks using the GitHub API.", + McpServers = new Dictionary + { + ["github"] = new McpLocalServerConfig + { + Type = "stdio", + Command = "npx", + Args = new[] { "-y", "@modelcontextprotocol/server-github" }, + Env = new Dictionary + { + ["GITHUB_TOKEN"] = Environment.GetEnvironmentVariable("GITHUB_TOKEN")! + }, + Tools = new[] { "*" }, + }, + }, + Infer = true, + }, + }, +}); +``` + +
+ +See [MCP Servers](mcp.md) for detailed MCP configuration. + +## Event Handling + +Track agent interactions with events: + +
+Node.js / TypeScript + +```typescript +import { SessionEventType } from "@github/copilot-sdk"; + +session.on((event) => { + switch (event.type) { + case SessionEventType.SubagentSelected: + console.log(`πŸ€– Agent selected: ${event.data.agentName}`); + break; + case SessionEventType.AssistantMessage: + console.log(`πŸ“ Response: ${event.data.content}`); + break; + case SessionEventType.ToolExecutionStart: + console.log(`πŸ”§ Tool: ${event.data.toolName}`); + break; + } +}); +``` + +
+ +
+Python + +```python +from copilot.types import SessionEventType + +def handle_event(event): + if event.type == SessionEventType.SUBAGENT_SELECTED: + print(f"πŸ€– Agent selected: {event.data.agent_name}") + elif event.type == SessionEventType.ASSISTANT_MESSAGE: + print(f"πŸ“ Response: {event.data.content}") + elif event.type == SessionEventType.TOOL_EXECUTION_START: + print(f"πŸ”§ Tool: {event.data.tool_name}") + +session.on(handle_event) +``` + +
+ +
+Go + +```go +session.On(func(event copilot.SessionEvent) { + switch event.Type { + case copilot.SessionEventTypeSubagentSelected: + fmt.Printf("πŸ€– Agent selected: %s\n", *event.Data.AgentName) + case copilot.SessionEventTypeAssistantMessage: + fmt.Printf("πŸ“ Response: %s\n", *event.Data.Content) + case copilot.SessionEventTypeToolExecutionStart: + fmt.Printf("πŸ”§ Tool: %s\n", *event.Data.ToolName) + } +}) +``` + +
+ +
+.NET + +```csharp +using GitHub.Copilot.SDK; + +session.On(e => +{ + switch (e.Type) + { + case SessionEventType.SubagentSelected: + Console.WriteLine($"πŸ€– Agent selected: {e.Data.AgentName}"); + break; + case SessionEventType.AssistantMessage: + Console.WriteLine($"πŸ“ Response: {e.Data.Content}"); + break; + case SessionEventType.ToolExecutionStart: + Console.WriteLine($"πŸ”§ Tool: {e.Data.ToolName}"); + break; + } +}); +``` + +
+ +## Best Practices + +1. **Clear prompts**: Be specific about the agent's expertise and behavior +2. **Focused scope**: Each agent should have a clear, single purpose +3. **Descriptive names**: Use memorable, action-oriented names like `code-reviewer` +4. **Appropriate tools**: Restrict tools to only what the agent needs +5. **Enable infer**: Set `infer: true` for seamless automatic selection +6. **Test prompts**: Iterate on system prompts for best results + +## Common Agent Patterns + +| Agent Type | Use Case | Key Prompt Elements | +|------------|----------|---------------------| +| Code Reviewer | Review code for bugs/style | Focus areas, severity levels, output format | +| SQL Expert | Database queries and optimization | Supported DBs, explain approach, warn about issues | +| Doc Writer | Technical documentation | Formats (Markdown, JSDoc), audience, structure | +| Security Auditor | Find vulnerabilities | OWASP Top 10, CVE references, remediation steps | +| API Designer | REST/GraphQL design | Best practices, versioning, error handling | + +## Related Resources + +- [Getting Started Guide](./getting-started.md) - SDK basics and custom tools +- [MCP Servers](./mcp.md) - Extend agents with MCP tools + +## See Also + +- Language-specific cookbooks for practical examples: + - [Python Cookbook](../cookbook/python/custom-agents.md) + - [Node.js Cookbook](../cookbook/nodejs/README.md) + - [Go Cookbook](../cookbook/go/README.md) + - [.NET Cookbook](../cookbook/dotnet/README.md) diff --git a/docs/getting-started.md b/docs/getting-started.md index dc56b865..7e8a7fd8 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -275,7 +275,7 @@ Update `main.py`: import asyncio import sys from copilot import CopilotClient -from copilot.generated.session_events import SessionEventType +from copilot.types import SessionEventType async def main(): client = CopilotClient() @@ -457,7 +457,7 @@ import random import sys from copilot import CopilotClient from copilot.tools import define_tool -from copilot.generated.session_events import SessionEventType +from copilot.types import SessionEventType from pydantic import BaseModel, Field # Define the parameters for the tool using Pydantic @@ -728,7 +728,7 @@ import random import sys from copilot import CopilotClient from copilot.tools import define_tool -from copilot.generated.session_events import SessionEventType +from copilot.types import SessionEventType from pydantic import BaseModel, Field class GetWeatherParams(BaseModel): diff --git a/docs/mcp.md b/docs/mcp.md index b67dd7ca..9dbea5ee 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -160,6 +160,9 @@ await using var session = await client.CreateSessionAsync(new SessionConfig Here's a complete working example using the official [`@modelcontextprotocol/server-filesystem`](https://www.npmjs.com/package/@modelcontextprotocol/server-filesystem) MCP server: +
+Node.js / TypeScript + ```typescript import { CopilotClient } from "@github/copilot-sdk"; @@ -195,6 +198,135 @@ async function main() { main(); ``` +
+ +
+Python + +```python +import asyncio +from copilot import CopilotClient + +async def main(): + client = CopilotClient() + await client.start() + + # Create session with filesystem MCP server + session = await client.create_session({ + "mcp_servers": { + "filesystem": { + "type": "local", + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"], + "tools": ["*"], + }, + }, + }) + + print(f"Session created: {session.session_id}") + + # The model can now use filesystem tools + result = await session.send_and_wait({ + "prompt": "List the files in the allowed directory" + }) + + print(f"Response: {result.data.content}") + + await session.destroy() + await client.stop() + +asyncio.run(main()) +``` + +
+ +
+Go + +```go +package main + +import ( + "fmt" + "log" + + copilot "github.com/github/copilot-sdk/go" +) + +func main() { + client := copilot.NewClient(nil) + if err := client.Start(); err != nil { + log.Fatal(err) + } + defer client.Stop() + + // Create session with filesystem MCP server + session, err := client.CreateSession(&copilot.SessionConfig{ + MCPServers: map[string]copilot.MCPServerConfig{ + "filesystem": { + Type: "local", + Command: "npx", + Args: []string{"-y", "@modelcontextprotocol/server-filesystem", "/tmp"}, + Tools: []string{"*"}, + }, + }, + }) + if err != nil { + log.Fatal(err) + } + + fmt.Println("Session created:", session.SessionID) + + // The model can now use filesystem tools + result, err := session.SendAndWait(copilot.MessageOptions{ + Prompt: "List the files in the allowed directory", + }, 0) + if err != nil { + log.Fatal(err) + } + + fmt.Println("Response:", *result.Data.Content) +} +``` + +
+ +
+.NET + +```csharp +using GitHub.Copilot.SDK; + +await using var client = new CopilotClient(); + +// Create session with filesystem MCP server +await using var session = await client.CreateSessionAsync(new SessionConfig +{ + McpServers = new Dictionary + { + ["filesystem"] = new McpLocalServerConfig + { + Type = "local", + Command = "npx", + Args = new[] { "-y", "@modelcontextprotocol/server-filesystem", "/tmp" }, + Tools = new[] { "*" }, + }, + }, +}); + +Console.WriteLine($"Session created: {session.SessionId}"); + +// The model can now use filesystem tools +var result = await session.SendAndWaitAsync(new MessageOptions +{ + Prompt = "List the files in the allowed directory" +}); + +Console.WriteLine($"Response: {result?.Data?.Content}"); +``` + +
+ **Output:** ``` Session created: 18b3482b-bcba-40ba-9f02-ad2ac949a59a diff --git a/dotnet/README.md b/dotnet/README.md index e176da40..c63bb120 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -452,9 +452,9 @@ try var session = await client.CreateSessionAsync(); await session.SendAsync(new MessageOptions { Prompt = "Hello" }); } -catch (StreamJsonRpc.RemoteInvocationException ex) +catch (IOException ex) { - Console.Error.WriteLine($"JSON-RPC Error: {ex.Message}"); + Console.Error.WriteLine($"Communication Error: {ex.Message}"); } catch (Exception ex) { diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index ef7982cb..88946eef 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -347,8 +347,8 @@ public async Task CreateSessionAsync(SessionConfig? config = nul config?.DisabledSkills, config?.InfiniteSessions); - var response = await connection.Rpc.InvokeWithCancellationAsync( - "session.create", [request], cancellationToken); + var response = await InvokeRpcAsync( + connection.Rpc, "session.create", [request], cancellationToken); var session = new CopilotSession(response.SessionId, connection.Rpc, response.WorkspacePath); session.RegisterTools(config?.Tools ?? []); @@ -404,8 +404,8 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes config?.SkillDirectories, config?.DisabledSkills); - var response = await connection.Rpc.InvokeWithCancellationAsync( - "session.resume", [request], cancellationToken); + var response = await InvokeRpcAsync( + connection.Rpc, "session.resume", [request], cancellationToken); var session = new CopilotSession(response.SessionId, connection.Rpc, response.WorkspacePath); session.RegisterTools(config?.Tools ?? []); @@ -461,8 +461,8 @@ public async Task PingAsync(string? message = null, CancellationTo { var connection = await EnsureConnectedAsync(cancellationToken); - return await connection.Rpc.InvokeWithCancellationAsync( - "ping", [new PingRequest { Message = message }], cancellationToken); + return await InvokeRpcAsync( + connection.Rpc, "ping", [new PingRequest { Message = message }], cancellationToken); } /// @@ -475,8 +475,8 @@ public async Task GetStatusAsync(CancellationToken cancellati { var connection = await EnsureConnectedAsync(cancellationToken); - return await connection.Rpc.InvokeWithCancellationAsync( - "status.get", [], cancellationToken); + return await InvokeRpcAsync( + connection.Rpc, "status.get", [], cancellationToken); } /// @@ -489,8 +489,8 @@ public async Task GetAuthStatusAsync(CancellationToken ca { var connection = await EnsureConnectedAsync(cancellationToken); - return await connection.Rpc.InvokeWithCancellationAsync( - "auth.getStatus", [], cancellationToken); + return await InvokeRpcAsync( + connection.Rpc, "auth.getStatus", [], cancellationToken); } /// @@ -503,8 +503,8 @@ public async Task> ListModelsAsync(CancellationToken cancellatio { var connection = await EnsureConnectedAsync(cancellationToken); - var response = await connection.Rpc.InvokeWithCancellationAsync( - "models.list", [], cancellationToken); + var response = await InvokeRpcAsync( + connection.Rpc, "models.list", [], cancellationToken); return response.Models; } @@ -528,8 +528,8 @@ public async Task> ListModelsAsync(CancellationToken cancellatio { var connection = await EnsureConnectedAsync(cancellationToken); - var response = await connection.Rpc.InvokeWithCancellationAsync( - "session.getLastId", [], cancellationToken); + var response = await InvokeRpcAsync( + connection.Rpc, "session.getLastId", [], cancellationToken); return response.SessionId; } @@ -554,8 +554,8 @@ public async Task DeleteSessionAsync(string sessionId, CancellationToken cancell { var connection = await EnsureConnectedAsync(cancellationToken); - var response = await connection.Rpc.InvokeWithCancellationAsync( - "session.delete", [new DeleteSessionRequest(sessionId)], cancellationToken); + var response = await InvokeRpcAsync( + connection.Rpc, "session.delete", [new DeleteSessionRequest(sessionId)], cancellationToken); if (!response.Success) { @@ -584,12 +584,24 @@ public async Task> ListSessionsAsync(CancellationToken can { var connection = await EnsureConnectedAsync(cancellationToken); - var response = await connection.Rpc.InvokeWithCancellationAsync( - "session.list", [], cancellationToken); + var response = await InvokeRpcAsync( + connection.Rpc, "session.list", [], cancellationToken); return response.Sessions; } + internal static async Task InvokeRpcAsync(JsonRpc rpc, string method, object?[]? args, CancellationToken cancellationToken) + { + try + { + return await rpc.InvokeWithCancellationAsync(method, args, cancellationToken); + } + catch (StreamJsonRpc.RemoteRpcException ex) + { + throw new IOException($"Communication error with Copilot CLI: {ex.Message}", ex); + } + } + private Task EnsureConnectedAsync(CancellationToken cancellationToken) { if (_connectionTask is null && !_options.AutoStart) @@ -604,8 +616,8 @@ private Task EnsureConnectedAsync(CancellationToken cancellationToke private async Task VerifyProtocolVersionAsync(Connection connection, CancellationToken cancellationToken) { var expectedVersion = SdkProtocolVersion.GetVersion(); - var pingResponse = await connection.Rpc.InvokeWithCancellationAsync( - "ping", [new PingRequest()], cancellationToken); + var pingResponse = await InvokeRpcAsync( + connection.Rpc, "ping", [new PingRequest()], cancellationToken); if (!pingResponse.ProtocolVersion.HasValue) { diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index f1e47df8..7f1cc4e4 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -80,6 +80,9 @@ internal CopilotSession(string sessionId, JsonRpc rpc, string? workspacePath = n WorkspacePath = workspacePath; } + private Task InvokeRpcAsync(string method, object?[]? args, CancellationToken cancellationToken) => + CopilotClient.InvokeRpcAsync(_rpc, method, args, cancellationToken); + /// /// Sends a message to the Copilot session and waits for the response. /// @@ -118,7 +121,7 @@ public async Task SendAsync(MessageOptions options, CancellationToken ca Mode = options.Mode }; - var response = await _rpc.InvokeWithCancellationAsync( + var response = await InvokeRpcAsync( "session.send", [request], cancellationToken); return response.MessageId; @@ -351,7 +354,7 @@ internal async Task HandlePermissionRequestAsync(JsonEl /// public async Task> GetMessagesAsync(CancellationToken cancellationToken = default) { - var response = await _rpc.InvokeWithCancellationAsync( + var response = await InvokeRpcAsync( "session.getMessages", [new GetMessagesRequest { SessionId = SessionId }], cancellationToken); return response.Events @@ -385,7 +388,7 @@ public async Task> GetMessagesAsync(CancellationToke /// public async Task AbortAsync(CancellationToken cancellationToken = default) { - await _rpc.InvokeWithCancellationAsync( + await InvokeRpcAsync( "session.abort", [new SessionAbortRequest { SessionId = SessionId }], cancellationToken); } @@ -416,8 +419,8 @@ await _rpc.InvokeWithCancellationAsync( /// public async ValueTask DisposeAsync() { - await _rpc.InvokeWithCancellationAsync( - "session.destroy", [new SessionDestroyRequest() { SessionId = SessionId }]); + await InvokeRpcAsync( + "session.destroy", [new SessionDestroyRequest() { SessionId = SessionId }], CancellationToken.None); _eventHandlers.Clear(); _toolHandlers.Clear(); diff --git a/dotnet/test/SessionTests.cs b/dotnet/test/SessionTests.cs index 845e604a..13b23522 100644 --- a/dotnet/test/SessionTests.cs +++ b/dotnet/test/SessionTests.cs @@ -26,7 +26,7 @@ public async Task ShouldCreateAndDestroySessions() await session.DisposeAsync(); - var ex = await Assert.ThrowsAsync(() => session.GetMessagesAsync()); + var ex = await Assert.ThrowsAsync(() => session.GetMessagesAsync()); Assert.Contains("not found", ex.Message, StringComparison.OrdinalIgnoreCase); } @@ -192,7 +192,7 @@ public async Task Should_Resume_A_Session_Using_A_New_Client() [Fact] public async Task Should_Throw_Error_When_Resuming_Non_Existent_Session() { - await Assert.ThrowsAsync(() => + await Assert.ThrowsAsync(() => Client.ResumeSessionAsync("non-existent-session-id")); } diff --git a/python/copilot/types.py b/python/copilot/types.py index bb64dd98..c0b1798c 100644 --- a/python/copilot/types.py +++ b/python/copilot/types.py @@ -12,6 +12,7 @@ # Import generated SessionEvent types from .generated.session_events import SessionEvent +from .generated.session_events import SessionEventType as SessionEventType # SessionEvent is now imported from generated types # It provides proper type discrimination for all event types @@ -219,7 +220,9 @@ class SessionConfig(TypedDict, total=False): """Configuration for creating a session""" session_id: str # Optional custom session ID - model: Literal["gpt-5", "claude-sonnet-4", "claude-sonnet-4.5", "claude-haiku-4.5"] + model: Literal["claude-sonnet-4.5", "claude-haiku-4.5", "claude-opus-4.5", "claude-sonnet-4", + "gpt-5.2-codex", "gpt-5.1-codex-max", "gpt-5.1-codex", "gpt-5.2", "gpt-5.1", + "gpt-5", "gpt-5.1-codex-mini", "gpt-5-mini", "gpt-4.1", "gemini-3-pro-preview"] tools: list[Tool] system_message: SystemMessageConfig # System message configuration # List of tool names to allow (takes precedence over excluded_tools) diff --git a/python/pyproject.toml b/python/pyproject.toml index 3a724120..094493f4 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -21,6 +21,11 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Operating System :: OS Independent", + "Topic :: Software Development :: Libraries :: Python Modules", + "Typing :: Typed", ] dependencies = [ "python-dateutil>=2.9.0.post0", diff --git a/python/setup.py b/python/setup.py index cef01148..354398a2 100644 --- a/python/setup.py +++ b/python/setup.py @@ -7,5 +7,5 @@ install_requires=[ "typing-extensions>=4.0.0", ], - python_requires=">=3.8", + python_requires=">=3.9", )