diff --git a/README.md b/README.md index 2812d1e..351442f 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ Will output: } ``` -for claude, you should pass 2nd argument as SchemaFormat.claude or `claude`: +For claude, you should pass 2nd argument as SchemaFormat.claude or `claude`: ```python from function_schema import get_function_schema @@ -90,28 +90,13 @@ Please refer to the [Claude tool use](https://docs.anthropic.com/claude/docs/too You can use any type hinting supported by python for the first argument of `Annotated`. including: `typing.Literal`, `typing.Optional`, `typing.Union`, and `T | None` for python 3.10+. `Doc` class or plain string in `Annotated` is used for describe the parameter. -`Doc` metadata is the [PEP propose](https://peps.python.org/pep-0727/) for standardizing the metadata in type hints. -currently, implemented in `typing-extensions` module. Also `function_schema.Doc` is provided for compatibility. Enumeratable candidates can be defined with `enum.Enum` in the argument of `Annotated`. +In shorthand, you can use `typing.Literal` as the type will do the same thing: ```python -import enum - -class AnimalType(enum.Enum): - dog = enum.auto() - cat = enum.auto() - -def get_animal( - animal: Annotated[str, Doc("The animal to get"), AnimalType], -) -> str: - """Returns the animal.""" - return f"Animal is {animal.value}" -``` -In this example, each name of `AnimalType` enums(`dog`, `cat`) is used as an enum schema. -In shorthand, you can use `typing.Literal` as the type will do the same thing. +from typing import Annotated, Literal -```python def get_animal( animal: Annotated[Literal["dog", "cat"], Doc("The animal to get")], ) -> str: @@ -119,85 +104,21 @@ def get_animal( return f"Animal is {animal}" ``` - -### Plain String in Annotated - -The string value of `Annotated` is used as a description for convenience. - -```python -def get_weather( - city: Annotated[str, "The city to get the weather for"], # <- string value of Annotated is used as a description - unit: Annotated[Optional[str], "The unit to return the temperature in"] = "celcius", -) -> str: - """Returns the weather for the given city.""" - return f"Weather for {city} is 20°C" -``` - -But this would create a predefined meaning for any plain string inside of `Annotated`, -and any tool that was using plain strings in them for any other purpose, which is currently allowed, would now be invalid. -Please refer to the [PEP 0727, Plain String in Annotated](https://peps.python.org/pep-0727/#plain-string-in-annotated) for more information. - -### Usage with OpenAI API - -You can use this schema to make a function call in OpenAI API: -```python -import openai -openai.api_key = "sk-..." - -# Create an assistant with the function -assistant = client.beta.assistants.create( - instructions="You are a weather bot. Use the provided functions to answer questions.", - model="gpt-4-turbo-preview", - tools=[{ - "type": "function", - "function": get_function_schema(get_weather), - }] -) - -run = client.beta.messages.create( - assistant_id=assistant.id, - messages=[ - {"role": "user", "content": "What's the weather like in Seoul?"} - ] -) - -# or with chat completion - -result = openai.chat.completion.create( - model="gpt-3.5-turbo", - messages=[ - {"role": "user", "content": "What's the weather like in Seoul?"} - ], - tools=[{ - "type": "function", - "function": get_function_schema(get_weather) - }], - tool_call="auto", -) -``` - -### Usage with Anthropic Claude - -```python -import anthropic - -client = anthropic.Client() - -response = client.beta.tools.messages.create( - model="claude-3-opus-20240229", - max_tokens=4096, - tools=[get_function_schema(get_weather, "claude")], - messages=[ - {"role": "user", "content": "What's the weather like in Seoul?"} - ] -) -``` - ### CLI usage ```sh function_schema mymodule.py my_function | jq ``` +### More Examples + +For comprehensive usage examples with different AI platforms, see the [examples directory](./examples/): + +- **[Basic Usage](./examples/basic_usage.py)** - Core features and function definition patterns +- **[OpenAI Integration](./examples/openai_example.py)** - Assistant API and Chat Completion examples +- **[Claude Integration](./examples/claude_example.py)** - Anthropic Claude tool calling examples +- **[MCP Integration](./examples/mcp_example.py)** - Model Context Protocol examples +- **[CLI Usage](./examples/cli_example.py)** - Command-line interface examples + ## License MIT License diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..925a3eb --- /dev/null +++ b/examples/README.md @@ -0,0 +1,95 @@ +# Function Schema Examples + +This directory contains practical examples of how to use the `function-schema` library with various AI platforms and protocols. + +## Examples + +### 🔧 Basic Usage (`basic_usage.py`) +Demonstrates the fundamental features of function-schema: +- Creating functions with type annotations and Doc metadata +- Generating JSON schemas +- Using enums and Literal types for parameter constraints +- Different annotation styles + +**Run:** `python examples/basic_usage.py` + +### 🤖 OpenAI Integration (`openai_example.py`) +Shows how to integrate with OpenAI's APIs: +- Assistant API with tool calling +- Chat Completion API with function calling +- Multiple tool definitions + +**Run:** `python examples/openai_example.py` + +### 🧠 Claude Integration (`claude_example.py`) +Demonstrates Anthropic Claude tool calling: +- Basic tool calling setup +- Multi-turn conversations with tools +- Claude-specific schema format + +**Run:** `python examples/claude_example.py` + +### 📟 CLI Usage (`cli_example.py`) +Examples of using the command-line interface: +- Generating schemas from Python files +- Different output formats +- Working with multiple functions + +**Test the CLI:** +```bash +# Install the package first +pip install -e . + +# Generate schema for a function +function_schema examples/cli_example.py get_weather + +# Generate with pretty JSON formatting +function_schema examples/cli_example.py get_weather | jq + +# Generate for Claude format +function_schema examples/cli_example.py get_weather claude +``` + +### 🔌 MCP Integration (`mcp_example.py`) +Shows integration with Model Context Protocol: +- Creating MCP-compatible tool definitions +- Server manifest generation +- Tool calling examples +- Resource access patterns + +**Run:** `python examples/mcp_example.py` + +## Running the Examples + +1. **Install the package:** + ```bash + pip install -e . + ``` + +2. **Run any example:** + ```bash + python examples/basic_usage.py + python examples/openai_example.py + python examples/claude_example.py + python examples/mcp_example.py + ``` + +3. **Test CLI functionality:** + ```bash + function_schema examples/cli_example.py get_weather + ``` + +## Integration Notes + +- **OpenAI**: Requires `openai` library and API key for actual usage +- **Claude**: Requires `anthropic` library and API key for actual usage +- **MCP**: Conceptual example showing schema compatibility +- **CLI**: Works out of the box with the installed package + +## Schema Formats + +The library supports multiple output formats: +- **OpenAI format** (default): Uses `parameters` key +- **Claude format**: Uses `input_schema` key + +Specify format with: `get_function_schema(func, "claude")` \ No newline at end of file diff --git a/examples/basic_usage.py b/examples/basic_usage.py new file mode 100644 index 0000000..561fe24 --- /dev/null +++ b/examples/basic_usage.py @@ -0,0 +1,88 @@ +""" +Basic usage example for function-schema library. + +This example demonstrates how to: +1. Define a function with type annotations and Doc metadata +2. Generate a JSON schema from the function +3. Use enum for parameter constraints +""" + +from typing import Annotated, Optional, Literal +from function_schema import Doc, get_function_schema +import enum +import json + + +def get_weather( + city: Annotated[str, Doc("The city to get the weather for")], + unit: Annotated[ + Optional[str], + Doc("The unit to return the temperature in"), + enum.Enum("Unit", "celcius fahrenheit") + ] = "celcius", +) -> str: + """Returns the weather for the given city.""" + return f"Weather for {city} is 20°C" + + +def get_animal_with_enum( + animal: Annotated[str, Doc("The animal to get"), + enum.Enum("AnimalType", "dog cat")], +) -> str: + """Returns the animal using enum.""" + return f"Animal is {animal}" + + +class AnimalType(enum.Enum): + dog = enum.auto() + cat = enum.auto() + + +def get_animal_with_class_enum( + animal: Annotated[str, Doc("The animal to get"), AnimalType], +) -> str: + """Returns the animal using class-based enum.""" + return f"Animal is {animal.value}" + + +def get_animal_with_literal( + animal: Annotated[Literal["dog", "cat"], Doc("The animal to get")], +) -> str: + """Returns the animal using Literal type.""" + return f"Animal is {animal}" + + +def get_weather_with_string_annotation( + city: Annotated[str, "The city to get the weather for"], + unit: Annotated[Optional[str], "The unit to return the temperature in"] = "celcius", +) -> str: + """Returns the weather for the given city using plain string annotations.""" + return f"Weather for {city} is 20°C" + + +if __name__ == "__main__": + # Generate schema for the main weather function + schema = get_function_schema(get_weather) + print("Basic weather function schema:") + print(json.dumps(schema, indent=2)) + + print("\n" + "="*50 + "\n") + + # Generate schema for Claude format + claude_schema = get_function_schema(get_weather, "claude") + print("Schema for Claude:") + print(json.dumps(claude_schema, indent=2)) + + print("\n" + "="*50 + "\n") + + # Generate schema for enum example + enum_schema = get_function_schema(get_animal_with_enum) + print("Animal enum schema:") + print(json.dumps(enum_schema, indent=2)) + + print("\n" + "="*50 + "\n") + + # Generate schema for Literal example + literal_schema = get_function_schema(get_animal_with_literal) + print("Animal literal schema:") + print(json.dumps(literal_schema, indent=2)) \ No newline at end of file diff --git a/examples/claude_example.py b/examples/claude_example.py new file mode 100644 index 0000000..1d98740 --- /dev/null +++ b/examples/claude_example.py @@ -0,0 +1,171 @@ +""" +Anthropic Claude API integration example for function-schema library. + +This example demonstrates how to use function schemas with Claude's tool calling feature. + +Note: This example requires the anthropic library to be installed and an API key to be set. +For demonstration purposes, this shows the structure without actually making API calls. +""" + +from typing import Annotated, Optional +from function_schema import Doc, get_function_schema +import enum +import json + +# For actual usage, uncomment the following: +# import anthropic +# client = anthropic.Anthropic(api_key="your-api-key-here") + + +def get_weather( + city: Annotated[str, Doc("The city to get the weather for")], + unit: Annotated[ + Optional[str], + Doc("The unit to return the temperature in"), + enum.Enum("Unit", "celcius fahrenheit") + ] = "celcius", +) -> str: + """Returns the weather for the given city.""" + return f"Weather for {city} is 20°C" + + +def search_web( + query: Annotated[str, Doc("The search query")], + max_results: Annotated[Optional[int], Doc("Maximum number of results")] = 5 +) -> str: + """Search the web for information.""" + return f"Search results for '{query}': Found {max_results} results" + + +def calculate( + expression: Annotated[str, Doc("Mathematical expression to calculate")] +) -> str: + """Calculate a mathematical expression.""" + try: + # Note: In production, use a safer eval method + result = eval(expression) + return f"Result: {result}" + except Exception as e: + return f"Error calculating '{expression}': {str(e)}" + + +def claude_tool_calling_example(): + """Example of using function schema with Claude's tool calling feature.""" + + # Generate function schemas for Claude format + tools = [ + get_function_schema(get_weather, "claude"), + get_function_schema(search_web, "claude"), + get_function_schema(calculate, "claude") + ] + + print("Claude Tool Calling Example:") + print("Tool definitions for Claude:") + print(json.dumps(tools, indent=2)) + + print("\nExample Claude API usage:") + print(""" +import anthropic + +client = anthropic.Anthropic(api_key="your-api-key") + +response = client.beta.tools.messages.create( + model="claude-3-opus-20240229", + max_tokens=4096, + tools=[ + get_function_schema(get_weather, "claude"), + get_function_schema(search_web, "claude"), + get_function_schema(calculate, "claude") + ], + messages=[ + { + "role": "user", + "content": "What's the weather like in Seoul? Also search for 'Claude AI news' and calculate 15 * 24" + } + ] +) + +# Handle tool use in the response +if response.content: + for content_block in response.content: + if content_block.type == "tool_use": + tool_name = content_block.name + tool_args = content_block.input + + # Call the appropriate function + if tool_name == "get_weather": + result = get_weather(**tool_args) + elif tool_name == "search_web": + result = search_web(**tool_args) + elif tool_name == "calculate": + result = calculate(**tool_args) + + print(f"Tool {tool_name} result: {result}") +""") + + +def claude_conversational_example(): + """Example of a conversational Claude interaction with tools.""" + + print("\nClaude Conversational Example with Tools:") + print(""" +import anthropic + +client = anthropic.Anthropic(api_key="your-api-key") + +# Multi-turn conversation with tool use +messages = [ + {"role": "user", "content": "Can you help me plan a trip to Tokyo?"} +] + +response = client.beta.tools.messages.create( + model="claude-3-sonnet-20240229", + max_tokens=1024, + tools=[ + get_function_schema(get_weather, "claude"), + get_function_schema(search_web, "claude") + ], + messages=messages +) + +# Continue the conversation based on tool results +messages.append({"role": "assistant", "content": response.content}) + +# Claude might use tools to get weather info and search for travel tips +if any(block.type == "tool_use" for block in response.content): + # Process tool calls and add results to conversation + for content_block in response.content: + if content_block.type == "tool_use": + # Execute tool and add result + tool_result = execute_tool(content_block.name, content_block.input) + messages.append({ + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": content_block.id, + "content": tool_result + } + ] + }) + + # Get Claude's response after tool execution + final_response = client.beta.tools.messages.create( + model="claude-3-sonnet-20240229", + max_tokens=1024, + tools=[ + get_function_schema(get_weather, "claude"), + get_function_schema(search_web, "claude") + ], + messages=messages + ) +""") + + +if __name__ == "__main__": + print("Function Schema - Claude Integration Examples") + print("=" * 50) + + claude_tool_calling_example() + print("\n" + "=" * 50) + claude_conversational_example() \ No newline at end of file diff --git a/examples/cli_example.py b/examples/cli_example.py new file mode 100644 index 0000000..7f029e3 --- /dev/null +++ b/examples/cli_example.py @@ -0,0 +1,86 @@ +""" +CLI usage example for function-schema library. + +This example demonstrates how to use the command-line interface to generate +function schemas from Python files. +""" + +from typing import Annotated, Optional +from function_schema import Doc +import enum + + +def get_weather( + city: Annotated[str, Doc("The city to get the weather for")], + unit: Annotated[ + Optional[str], + Doc("The unit to return the temperature in"), + enum.Enum("Unit", "celcius fahrenheit") + ] = "celcius", +) -> str: + """Returns the weather for the given city.""" + return f"Weather for {city} is 20°C" + + +def calculate_distance( + lat1: Annotated[float, Doc("Latitude of first location")], + lon1: Annotated[float, Doc("Longitude of first location")], + lat2: Annotated[float, Doc("Latitude of second location")], + lon2: Annotated[float, Doc("Longitude of second location")], + unit: Annotated[Optional[str], Doc("Unit for distance (km or miles)")] = "km" +) -> float: + """Calculate distance between two geographic coordinates.""" + import math + + # Haversine formula + R = 6371 if unit == "km" else 3959 # Earth's radius + + lat1_rad = math.radians(lat1) + lat2_rad = math.radians(lat2) + delta_lat = math.radians(lat2 - lat1) + delta_lon = math.radians(lon2 - lon1) + + a = (math.sin(delta_lat/2)**2 + + math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(delta_lon/2)**2) + c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a)) + + return R * c + + +if __name__ == "__main__": + print("CLI Usage Examples for function-schema") + print("=" * 50) + + print("1. Generate schema for a function (default OpenAI format):") + print(" function_schema examples/cli_example.py get_weather") + print() + + print("2. Generate schema with JSON formatting:") + print(" function_schema examples/cli_example.py get_weather | jq") + print() + + print("3. Generate schema for Claude format:") + print(" function_schema examples/cli_example.py get_weather claude") + print() + + print("4. Generate schema for another function:") + print(" function_schema examples/cli_example.py calculate_distance") + print() + + print("5. Save schema to file:") + print(" function_schema examples/cli_example.py get_weather > weather_schema.json") + print() + + print("Try running these commands from the project root directory!") + print("Make sure the function-schema package is installed first:") + print(" pip install -e .") + print() + + print("Available functions in this file:") + import inspect + functions = [name for name, obj in locals().items() + if inspect.isfunction(obj) and not name.startswith('_')] + for func_name in functions: + func = locals()[func_name] + doc = inspect.getdoc(func) or "No description" + print(f" - {func_name}: {doc}") \ No newline at end of file diff --git a/examples/mcp_example.py b/examples/mcp_example.py new file mode 100644 index 0000000..c4a44ee --- /dev/null +++ b/examples/mcp_example.py @@ -0,0 +1,240 @@ +""" +MCP (Model Context Protocol) integration example for function-schema library. + +This example demonstrates how to use function schemas with the Model Context Protocol, +which is a standard for AI assistants to interact with external tools and data sources. + +MCP allows AI models to: +1. Access tools (functions) through a standardized interface +2. Read and write resources (files, databases, APIs) +3. Maintain context across interactions + +Note: This is a conceptual example showing how function-schema can be used +to generate tool definitions compatible with MCP. +""" + +from typing import Annotated, Optional, List, Dict, Any +from function_schema import Doc, get_function_schema +import enum +import json + + +class Priority(enum.Enum): + low = "low" + medium = "medium" + high = "high" + urgent = "urgent" + + +def read_file( + path: Annotated[str, Doc("File path to read")] +) -> str: + """Read contents of a file.""" + try: + with open(path, 'r', encoding='utf-8') as f: + return f.read() + except Exception as e: + return f"Error reading file: {str(e)}" + + +def write_file( + path: Annotated[str, Doc("File path to write to")], + content: Annotated[str, Doc("Content to write to the file")], + append: Annotated[Optional[bool], Doc("Whether to append to file")] = False +) -> str: + """Write content to a file.""" + try: + mode = 'a' if append else 'w' + with open(path, mode, encoding='utf-8') as f: + f.write(content) + return f"Successfully {'appended to' if append else 'wrote'} file: {path}" + except Exception as e: + return f"Error writing file: {str(e)}" + + +def list_directory( + path: Annotated[str, Doc("Directory path to list")], + show_hidden: Annotated[Optional[bool], Doc("Include hidden files")] = False +) -> List[str]: + """List contents of a directory.""" + import os + try: + items = os.listdir(path) + if not show_hidden: + items = [item for item in items if not item.startswith('.')] + return sorted(items) + except Exception as e: + return [f"Error listing directory: {str(e)}"] + + +def create_task( + title: Annotated[str, Doc("Task title")], + description: Annotated[Optional[str], Doc("Task description")] = "", + priority: Annotated[Optional[str], Doc("Task priority")] = "medium", + due_date: Annotated[Optional[str], Doc("Due date (YYYY-MM-DD format)")] = None +) -> str: + """Create a new task.""" + task = { + "title": title, + "description": description, + "priority": priority, + "due_date": due_date, + "completed": False + } + return f"Created task: {json.dumps(task, indent=2)}" + + +def search_web( + query: Annotated[str, Doc("Search query")], + max_results: Annotated[Optional[int], Doc("Maximum results to return")] = 10, + site: Annotated[Optional[str], Doc("Specific site to search within")] = None +) -> str: + """Search the web for information.""" + search_params = { + "query": query, + "max_results": max_results, + "site_filter": site + } + # This would integrate with a real search API + return f"Web search executed with params: {json.dumps(search_params, indent=2)}" + + +def send_email( + to: Annotated[str, Doc("Recipient email address")], + subject: Annotated[str, Doc("Email subject")], + body: Annotated[str, Doc("Email body content")], + cc: Annotated[Optional[str], Doc("CC recipients (comma-separated)")] = None, + priority: Annotated[Optional[str], Doc("Email priority (low, normal, high)")] = "normal" +) -> str: + """Send an email.""" + email_data = { + "to": to, + "subject": subject, + "body": body, + "cc": cc.split(",") if cc else [], + "priority": priority + } + return f"Email queued for sending: {json.dumps(email_data, indent=2)}" + + +def mcp_server_manifest(): + """Generate an MCP server manifest with available tools.""" + + # Define specific functions to include (avoid dynamic inspection issues) + functions_to_include = [ + read_file, + write_file, + list_directory, + create_task, + search_web, + send_email + ] + + # Generate tool definitions + tools = [] + for func in functions_to_include: + try: + schema = get_function_schema(func) + # Convert to MCP tool format + mcp_tool = { + "name": schema["name"], + "description": schema["description"], + "inputSchema": schema["parameters"] + } + tools.append(mcp_tool) + except Exception as e: + print(f"Warning: Could not generate schema for {func.__name__}: {e}") + continue + + # MCP server manifest + manifest = { + "name": "function-schema-example-server", + "version": "1.0.0", + "description": "Example MCP server using function-schema generated tools", + "tools": tools, + "resources": [ + { + "uri": "file:///*", + "name": "File System", + "description": "Access to local file system" + } + ], + "capabilities": { + "tools": { + "supportsProgressNotifications": True + }, + "resources": { + "subscribe": True, + "listChanged": True + } + } + } + + return manifest + + +def mcp_tool_call_example(): + """Example of how an MCP client might call tools.""" + + print("MCP Tool Call Examples:") + print("=" * 30) + + # Example tool calls in MCP format + examples = [ + { + "name": "read_file", + "arguments": {"path": "/tmp/example.txt"} + }, + { + "name": "create_task", + "arguments": { + "title": "Review MCP integration", + "description": "Test the Model Context Protocol integration", + "priority": "high", + "due_date": "2024-01-15" + } + }, + { + "name": "search_web", + "arguments": { + "query": "Model Context Protocol documentation", + "max_results": 5 + } + } + ] + + for example in examples: + print(f"\nTool Call: {example['name']}") + print(f"Arguments: {json.dumps(example['arguments'], indent=2)}") + + # Simulate tool execution + if example['name'] == 'read_file': + result = read_file(**example['arguments']) + elif example['name'] == 'create_task': + result = create_task(**example['arguments']) + elif example['name'] == 'search_web': + result = search_web(**example['arguments']) + + print(f"Result: {result}") + print("-" * 40) + + +if __name__ == "__main__": + print("Function Schema - MCP (Model Context Protocol) Integration") + print("=" * 60) + + # Generate and display MCP server manifest + manifest = mcp_server_manifest() + print("MCP Server Manifest:") + print(json.dumps(manifest, indent=2)) + + print("\n" + "=" * 60) + + # Show tool call examples + mcp_tool_call_example() + + print("\n" + "=" * 60) + print("This example demonstrates how function-schema can generate") + print("tool definitions compatible with the Model Context Protocol,") + print("enabling AI assistants to discover and use your functions") + print("through a standardized interface.") \ No newline at end of file diff --git a/examples/openai_example.py b/examples/openai_example.py new file mode 100644 index 0000000..024711e --- /dev/null +++ b/examples/openai_example.py @@ -0,0 +1,160 @@ +""" +OpenAI API integration example for function-schema library. + +This example demonstrates how to use function schemas with OpenAI's API for: +1. Assistant API with tool calling +2. Chat completion with function calling + +Note: This example requires the openai library to be installed and an API key to be set. +For demonstration purposes, this shows the structure without actually making API calls. +""" + +from typing import Annotated, Optional +from function_schema import Doc, get_function_schema +import enum +import json + +# For actual usage, uncomment the following: +# import openai +# openai.api_key = "sk-your-api-key-here" + + +def get_weather( + city: Annotated[str, Doc("The city to get the weather for")], + unit: Annotated[ + Optional[str], + Doc("The unit to return the temperature in"), + enum.Enum("Unit", "celcius fahrenheit") + ] = "celcius", +) -> str: + """Returns the weather for the given city.""" + return f"Weather for {city} is 20°C" + + +def get_current_time( + timezone: Annotated[Optional[str], Doc("Timezone (e.g., 'UTC', 'EST')")] = "UTC" +) -> str: + """Get the current time in the specified timezone.""" + from datetime import datetime + return f"Current time in {timezone}: {datetime.now().isoformat()}" + + +def openai_assistant_example(): + """Example of using function schema with OpenAI Assistant API.""" + + # Generate function schema for OpenAI format + weather_tool = { + "type": "function", + "function": get_function_schema(get_weather) + } + + time_tool = { + "type": "function", + "function": get_function_schema(get_current_time) + } + + print("OpenAI Assistant API Example:") + print("Tool definitions:") + print(json.dumps([weather_tool, time_tool], indent=2)) + + print("\nExample assistant creation code:") + print(""" +import openai + +client = openai.OpenAI(api_key="your-api-key") + +# Create an assistant with the function +assistant = client.beta.assistants.create( + instructions="You are a helpful assistant. Use the provided functions to answer questions.", + model="gpt-4-turbo-preview", + tools=[ + { + "type": "function", + "function": get_function_schema(get_weather), + }, + { + "type": "function", + "function": get_function_schema(get_current_time), + } + ] +) + +# Create a thread and run +thread = client.beta.threads.create() + +run = client.beta.threads.runs.create( + thread_id=thread.id, + assistant_id=assistant.id, + messages=[ + {"role": "user", "content": "What's the weather like in Seoul?"} + ] +) +""") + + +def openai_chat_completion_example(): + """Example of using function schema with OpenAI Chat Completion API.""" + + # Generate function schemas + tools = [ + { + "type": "function", + "function": get_function_schema(get_weather) + }, + { + "type": "function", + "function": get_function_schema(get_current_time) + } + ] + + print("\nOpenAI Chat Completion API Example:") + print("Tools for chat completion:") + print(json.dumps(tools, indent=2)) + + print("\nExample chat completion code:") + print(""" +import openai + +client = openai.OpenAI(api_key="your-api-key") + +response = client.chat.completions.create( + model="gpt-3.5-turbo", + messages=[ + {"role": "user", "content": "What's the weather like in Seoul and what time is it?"} + ], + tools=[ + { + "type": "function", + "function": get_function_schema(get_weather) + }, + { + "type": "function", + "function": get_function_schema(get_current_time) + } + ], + tool_choice="auto", +) + +# Handle tool calls in the response +if response.choices[0].message.tool_calls: + for tool_call in response.choices[0].message.tool_calls: + if tool_call.function.name == "get_weather": + # Parse arguments and call the actual function + import json + args = json.loads(tool_call.function.arguments) + result = get_weather(**args) + print(f"Weather result: {result}") + elif tool_call.function.name == "get_current_time": + args = json.loads(tool_call.function.arguments) + result = get_current_time(**args) + print(f"Time result: {result}") +""") + + +if __name__ == "__main__": + print("Function Schema - OpenAI Integration Examples") + print("=" * 50) + + openai_assistant_example() + print("\n" + "=" * 50) + openai_chat_completion_example() \ No newline at end of file diff --git a/function_schema/core.py b/function_schema/core.py index fdd0775..d74f086 100644 --- a/function_schema/core.py +++ b/function_schema/core.py @@ -22,6 +22,15 @@ UnionType = Union # type: ignore +def is_optional_type(T: Any) -> bool: + """Check if a type annotation represents an optional/nullable type.""" + origin = get_origin(T) + if origin is Union or origin is UnionType: + args = get_args(T) + return type(None) in args + return False + + __all__ = ("get_function_schema", "guess_type", "Doc", "Annotated") @@ -146,7 +155,7 @@ def get_function_schema( if ( get_origin(T) is not Literal - and not isinstance(None, T) + and not is_optional_type(T) and default_value is inspect._empty ): schema["required"].append(name) diff --git a/pyproject.toml b/pyproject.toml index 00252ac..fb380ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,9 @@ classifiers = [ "Development Status :: 3 - Alpha", "Programming Language :: Python :: 3.9", ] +dependencies = [ + "typing-extensions>=4.12.2; python_version<'3.11'" +] [project.urls] Homepage = "https://github.com/comfuture/function-schema" diff --git a/test/test_examples.py b/test/test_examples.py new file mode 100644 index 0000000..0a8b436 --- /dev/null +++ b/test/test_examples.py @@ -0,0 +1,81 @@ +""" +Test examples to ensure they are syntactically correct and runnable. +""" + +import os +import subprocess +import sys +from pathlib import Path +import pytest + + +def test_examples_syntax(): + """Test that all example files have valid Python syntax.""" + examples_dir = Path(__file__).parent.parent / "examples" + + for example_file in examples_dir.glob("*.py"): + # Compile the file to check syntax + with open(example_file, 'rb') as f: + try: + compile(f.read(), example_file, 'exec') + except SyntaxError as e: + pytest.fail(f"Syntax error in {example_file}: {e}") + + +def test_examples_importable(): + """Test that all example files can be imported without errors.""" + examples_dir = Path(__file__).parent.parent / "examples" + + for example_file in examples_dir.glob("*.py"): + # Skip files that might have special execution requirements + if example_file.name == "__init__.py": + continue + + # Test that the file can be imported (checks imports) + module_name = example_file.stem + import importlib.util + spec = importlib.util.spec_from_file_location(module_name, example_file) + try: + module = importlib.util.module_from_spec(spec) + # Execute the module to check imports (but not __main__ block) + old_name = module.__name__ + module.__name__ = "__not_main__" # Prevent __main__ block execution + spec.loader.exec_module(module) + module.__name__ = old_name + except ImportError as e: + # Only fail if it's importing function_schema - other imports are optional + if "function_schema" in str(e): + pytest.fail(f"Import error in {example_file}: {e}") + except Exception as e: + # Other exceptions during module execution are not import issues + pass + + +def test_cli_example_functions(): + """Test that CLI example can generate schemas for its functions.""" + import subprocess + import json + + # Test basic function schema generation + result = subprocess.run([ + sys.executable, "-m", "function_schema.cli", + "examples/cli_example.py", "get_weather" + ], capture_output=True, text=True, cwd=Path(__file__).parent.parent) + + assert result.returncode == 0, f"CLI failed: {result.stderr}" + + # Verify output is valid JSON + try: + schema = json.loads(result.stdout) + assert schema["name"] == "get_weather" + assert "parameters" in schema + except json.JSONDecodeError as e: + pytest.fail(f"CLI output is not valid JSON: {e}") + + +if __name__ == "__main__": + # Run tests if called directly + test_examples_syntax() + test_examples_importable() + test_cli_example_functions() + print("All example tests passed!") \ No newline at end of file