diff --git a/.codecov.yml b/.codecov.yml index 2c5188c..a3c1c3d 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -1,108 +1,41 @@ component_management: - default_rules: - paths: - - "intent_kit/**" - statuses: - project: - target: auto - threshold: 5% - base: auto - patch: - target: auto - threshold: 5% - base: auto individual_components: - - component_id: core_engine - name: Core Engine (Framework & Intent Graph) + - component_id: "core_engine" + name: "Core Engine" paths: - "intent_kit/graph/**" - "intent_kit/nodes/**" - statuses: - project: - target: auto - threshold: 5% - base: auto - patch: - target: auto - threshold: 5% - base: auto - - component_id: node_library - name: Node Library (Batteries Included) + - component_id: "llm_services" + name: "LLM Services" paths: - - "intent_kit/node_library/**" - statuses: - project: - target: auto - threshold: 5% - base: auto - patch: - target: auto - threshold: 5% - base: auto - - component_id: llm_services - name: LLM Services & Model Clients - paths: - - "intent_kit/services/ai/**" - - "intent_kit/services/yaml_service.py" - statuses: - project: - target: auto - threshold: 5% - base: auto - patch: - target: auto - threshold: 5% - base: auto - - component_id: eval_framework - name: Evaluation Framework + - "intent_kit/services/**" + - component_id: "eval_framework" + name: "Evaluation Framework" paths: - "intent_kit/evals/**" - statuses: - project: - target: auto - threshold: 5% - base: auto - patch: - target: auto - threshold: 5% - base: auto - - component_id: context_management - name: Context Management - paths: - - "intent_kit/context/**" - statuses: - project: - target: auto - threshold: 5% - base: auto - patch: - target: auto - threshold: 5% - base: auto - - component_id: utils - name: Utilities & Shared Logic + - component_id: "utils" + name: "Utilities & Shared Logic" paths: - "intent_kit/utils/**" - "intent_kit/types.py" - statuses: - project: - target: auto - threshold: 5% - base: auto - patch: - target: auto - threshold: 5% - base: auto - - component_id: error_handling - name: Error Handling + - component_id: "context" + name: "Context Management" + paths: + - "intent_kit/context/**" + - component_id: "node_library" + name: "Node Library" + paths: + - "intent_kit/node_library/**" + - component_id: "exceptions" + name: "Exceptions & Error Handling" paths: - "intent_kit/exceptions/**" - statuses: - project: - target: auto - threshold: 5% - base: auto - patch: - target: auto - threshold: 5% - base: auto + - component_id: "remediation" + name: "Remediation & Error Handling" + paths: + - "intent_kit/nodes/actions/remediation.py" + - "intent_kit/nodes/actions/argument_extractor.py" + - component_id: "testing" + name: "Testing" + paths: + - "tests/**" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8b5f8d1..3ed1434 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,6 +40,12 @@ repos: language: system files: pyproject.toml pass_filenames: false + - id: validate-codecov + name: Validate codecov.yml + entry: uv run scripts/validate_codecov.py + language: system + files: .codecov.yml + pass_filenames: false - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: diff --git a/README.md b/README.md index 20a0519..027fa4d 100644 --- a/README.md +++ b/README.md @@ -5,25 +5,28 @@

Intent Kit

Build reliable, auditable AI applications that understand user intent and take intelligent actions

-

- - CI Status +

+ + CI + + + Coverage Status - - Coverage + + Documentation - - PyPI Version + + PyPI + + + PyPI Downloads - PyPI Downloads - MIT License

Docs

- --- ## What is Intent Kit? @@ -90,6 +93,7 @@ greet = action( # Create a classifier to understand requests classifier = llm_classifier( name="main", + description="Route to appropriate action", children=[greet], llm_config={"provider": "openai", "model": "gpt-3.5-turbo"} ) @@ -100,6 +104,76 @@ result = graph.route("Hello Alice") print(result.output) # → "Hello Alice!" ``` +### 3. Using JSON Configuration + +For more complex workflows, use JSON configuration: + +```python +from intent_kit import IntentGraphBuilder + +# Define your functions +def greet(name, context=None): + return f"Hello {name}!" + +def calculate(operation, a, b, context=None): + if operation == "add": + return a + b + return None + +# Create function registry +function_registry = { + "greet": greet, + "calculate": calculate, +} + +# Define your graph in JSON +graph_config = { + "root": "main_classifier", + "nodes": { + "main_classifier": { + "id": "main_classifier", + "type": "classifier", + "classifier_type": "llm", + "name": "main_classifier", + "description": "Main intent classifier", + "llm_config": { + "provider": "openai", + "model": "gpt-3.5-turbo", + }, + "children": ["greet_action", "calculate_action"], + }, + "greet_action": { + "id": "greet_action", + "type": "action", + "name": "greet_action", + "description": "Greet the user", + "function": "greet", + "param_schema": {"name": "str"}, + }, + "calculate_action": { + "id": "calculate_action", + "type": "action", + "name": "calculate_action", + "description": "Perform a calculation", + "function": "calculate", + "param_schema": {"operation": "str", "a": "float", "b": "float"}, + }, + }, +} + +# Build your graph +graph = ( + IntentGraphBuilder() + .with_json(graph_config) + .with_functions(function_registry) + .build() +) + +# Test it! +result = graph.route("Hello Alice") +print(result.output) # → "Hello Alice!" +``` + --- ## How It Works diff --git a/docs/api/api-reference.md b/docs/api/api-reference.md index bf90891..9bd701c 100644 --- a/docs/api/api-reference.md +++ b/docs/api/api-reference.md @@ -1,3 +1,353 @@ # API Reference -::: intent_kit +This document provides a reference for the Intent Kit API. + +## Core Classes + +### IntentGraphBuilder + +The main builder class for creating intent graphs. + +```python +from intent_kit import IntentGraphBuilder +``` + +#### Methods + +##### `root(node)` +Set the root node for the graph. + +```python +graph = IntentGraphBuilder().root(classifier).build() +``` + +##### `with_json(json_graph)` +Configure the graph using JSON specification. + +```python +graph = ( + IntentGraphBuilder() + .with_json(graph_config) + .with_functions(function_registry) + .build() +) +``` + +##### `with_functions(function_registry)` +Register functions for use in actions. + +```python +function_registry = { + "greet": lambda name: f"Hello {name}!", + "calculate": lambda op, a, b: a + b if op == "add" else None, +} + +graph = ( + IntentGraphBuilder() + .with_json(graph_config) + .with_functions(function_registry) + .build() +) +``` + +##### `with_default_llm_config(config)` +Set default LLM configuration for the graph. + +```python +llm_config = { + "provider": "openai", + "model": "gpt-3.5-turbo", + "api_key": "your-api-key" +} + +graph = ( + IntentGraphBuilder() + .with_json(graph_config) + .with_functions(function_registry) + .with_default_llm_config(llm_config) + .build() +) +``` + +##### `with_debug_context(enabled=True)` +Enable debug context for execution tracking. + +```python +graph = ( + IntentGraphBuilder() + .with_json(graph_config) + .with_functions(function_registry) + .with_debug_context(True) + .build() +) +``` + +##### `with_context_trace(enabled=True)` +Enable context tracing for detailed execution logs. + +```python +graph = ( + IntentGraphBuilder() + .with_json(graph_config) + .with_functions(function_registry) + .with_context_trace(True) + .build() +) +``` + +##### `build()` +Build and return the IntentGraph instance. + +```python +graph = IntentGraphBuilder().root(classifier).build() +``` + +## Node Factory Functions + +### action() + +Create an action node. + +```python +from intent_kit import action + +greet_action = action( + name="greet", + description="Greet the user by name", + action_func=lambda name: f"Hello {name}!", + param_schema={"name": str} +) +``` + +#### Parameters + +- **name** (str): Unique identifier for the action +- **description** (str): Human-readable description +- **action_func** (callable): Function to execute +- **param_schema** (dict): Parameter type definitions + +### llm_classifier() + +Create an LLM classifier node. + +```python +from intent_kit import llm_classifier + +classifier = llm_classifier( + name="main", + description="Route to appropriate action", + children=[greet_action, weather_action], + llm_config={"provider": "openai", "model": "gpt-3.5-turbo"} +) +``` + +#### Parameters + +- **name** (str): Unique identifier for the classifier +- **description** (str): Human-readable description +- **children** (list): List of child nodes +- **llm_config** (dict): LLM configuration + +## JSON Configuration + +### Graph Structure + +```json +{ + "root": "main_classifier", + "nodes": { + "main_classifier": { + "id": "main_classifier", + "type": "classifier", + "classifier_type": "llm", + "name": "main_classifier", + "description": "Main intent classifier", + "llm_config": { + "provider": "openai", + "model": "gpt-3.5-turbo" + }, + "children": ["greet_action", "weather_action"] + }, + "greet_action": { + "id": "greet_action", + "type": "action", + "name": "greet_action", + "description": "Greet the user", + "function": "greet", + "param_schema": {"name": "str"} + }, + "weather_action": { + "id": "weather_action", + "type": "action", + "name": "weather_action", + "description": "Get weather information", + "function": "weather", + "param_schema": {"city": "str"} + } + } +} +``` + +### Node Types + +#### Classifier Nodes + +```json +{ + "id": "classifier_id", + "type": "classifier", + "classifier_type": "llm", + "name": "classifier_name", + "description": "Classifier description", + "llm_config": { + "provider": "openai", + "model": "gpt-3.5-turbo", + "api_key": "your-api-key" + }, + "children": ["action1", "action2"] +} +``` + +#### Action Nodes + +```json +{ + "id": "action_id", + "type": "action", + "name": "action_name", + "description": "Action description", + "function": "function_name", + "param_schema": { + "param1": "str", + "param2": "int" + } +} +``` + +## LLM Configuration + +### Supported Providers + +#### OpenAI + +```python +llm_config = { + "provider": "openai", + "model": "gpt-3.5-turbo", + "api_key": "your-openai-api-key" +} +``` + +#### Anthropic + +```python +llm_config = { + "provider": "anthropic", + "model": "claude-3-sonnet-20240229", + "api_key": "your-anthropic-api-key" +} +``` + +#### Google AI + +```python +llm_config = { + "provider": "google", + "model": "gemini-pro", + "api_key": "your-google-api-key" +} +``` + +#### Ollama + +```python +llm_config = { + "provider": "ollama", + "model": "llama2", + "base_url": "http://localhost:11434" +} +``` + +#### OpenRouter + +```python +llm_config = { + "provider": "openrouter", + "model": "mistralai/ministral-8b", + "api_key": "your-openrouter-api-key" +} +``` + +## Graph Execution + +### Routing Input + +```python +# Route user input through the graph +result = graph.route("Hello Alice") +print(result.output) # → "Hello Alice!" +``` + +### Execution Result + +The `route()` method returns an execution result object with: + +- **output**: The result of the action execution +- **node_path**: The path of nodes that were executed +- **parameters**: The extracted parameters +- **metadata**: Additional execution metadata + +## Error Handling + +### Common Errors + +#### Missing Functions + +```python +# Error: Function not found in registry +function_registry = {"greet": greet_func} +# Missing "weather" function referenced in JSON +``` + +#### Invalid JSON Configuration + +```python +# Error: Invalid node type +{ + "type": "invalid_type" # Must be "classifier" or "action" +} +``` + +#### Missing Required Parameters + +```python +# Error: Missing required parameter +param_schema = {"name": "str"} +# Input doesn't contain name parameter +``` + +## Best Practices + +### Function Registry + +- Register all functions referenced in your JSON configuration +- Use descriptive function names +- Include proper error handling in your functions + +### JSON Configuration + +- Use descriptive node names and IDs +- Provide clear descriptions for all nodes +- Validate your JSON configuration before deployment + +### LLM Configuration + +- Store API keys securely (use environment variables) +- Choose appropriate models for your use case +- Monitor API usage and costs + +### Error Handling + +- Always handle potential errors in your action functions +- Provide meaningful error messages +- Test with various input scenarios diff --git a/docs/concepts/intent-graphs.md b/docs/concepts/intent-graphs.md index db6db80..d858a97 100644 --- a/docs/concepts/intent-graphs.md +++ b/docs/concepts/intent-graphs.md @@ -60,11 +60,8 @@ main_classifier = llm_classifier( llm_config={"provider": "openai", "model": "gpt-4"} ) -# Build graph with LLM configuration -graph = IntentGraphBuilder().root(main_classifier).with_default_llm_config({ - "provider": "openai", - "model": "gpt-4" -}).build() +# Build graph +graph = IntentGraphBuilder().root(main_classifier).build() ``` ### Using JSON Configuration @@ -72,128 +69,157 @@ graph = IntentGraphBuilder().root(main_classifier).with_default_llm_config({ ```python from intent_kit import IntentGraphBuilder +# Define your functions +def greet(name, context=None): + return f"Hello {name}!" + +def weather(city, context=None): + return f"Weather in {city} is sunny" + +# Create function registry +function_registry = { + "greet": greet, + "weather": weather, +} + +# Define your graph in JSON json_graph = { "root": "main_classifier", "nodes": { "main_classifier": { - "id": "main_classifier", # Explicit ID (optional - defaults to 'name') - "type": "llm_classifier", + "id": "main_classifier", + "type": "classifier", + "classifier_type": "llm", "name": "main_classifier", "description": "Main intent classifier", "children": ["greet_action", "weather_action"], "llm_config": {"provider": "openai", "model": "gpt-4"} }, "greet_action": { - "id": "greet_action", # Explicit ID + "id": "greet_action", "type": "action", "name": "greet_action", "description": "Greet the user", - "function": "greet_function", + "function": "greet", "param_schema": {"name": "str"} }, "weather_action": { - "type": "action", # No explicit ID - defaults to 'name' + "id": "weather_action", + "type": "action", "name": "weather_action", "description": "Get weather information", - "function": "weather_function", + "function": "weather", "param_schema": {"city": "str"} } } } -function_registry = { - "greet_function": lambda name: f"Hello {name}!", - "weather_function": lambda city: f"Weather in {city} is sunny" -} - -graph = IntentGraphBuilder().with_json(json_graph).with_functions(function_registry).build() +# Build graph +graph = ( + IntentGraphBuilder() + .with_json(json_graph) + .with_functions(function_registry) + .build() +) ``` -**Node ID Behavior:** -- Each node can have an explicit `"id"` field -- If `"id"` is not provided, it defaults to the `"name"` field -- At least one of `"id"` or `"name"` must be present -- The `"id"` is used for internal node references and child relationships +## Graph Execution -## Execution Flow - -1. **Input Processing** - User input is received -2. **Root Classification** - Input is classified by root nodes -3. **Intent Routing** - Input is routed to appropriate actions -4. **Parameter Extraction** - Parameters are extracted from input -5. **Action Execution** - Action functions are executed -6. **Output Generation** - Structured output is returned - -## Multi-Intent Routing - -Intent graphs can handle multiple nodes in a single user input using splitter nodes: +### Routing Input ```python -from intent_kit import rule_splitter_node +# Route user input through the graph +result = graph.route("Hello Alice") +print(result.output) # → "Hello Alice!" -splitter = rule_splitter_node( - name="multi_split", - children=[greet_action, weather_action] -) +result = graph.route("What's the weather in San Francisco?") +print(result.output) # → "Weather in San Francisco is sunny" +``` -graph = IntentGraphBuilder().root(splitter).build() +### Execution Flow -# Handle: "Hello Alice and what's the weather in Paris?" -result = graph.route("Hello Alice and what's the weather in Paris?") -# Output: {"greet": "Hello Alice!", "weather": "Weather in Paris is sunny"} -``` +1. **Input Processing** - User input is received +2. **Classification** - Root classifier determines intent +3. **Parameter Extraction** - LLM extracts parameters from input +4. **Action Execution** - Selected action runs with parameters +5. **Output Generation** - Action result is returned -## Context Management +## Graph Validation -Intent graphs support stateful conversations through context: +### Built-in Validation -```python -from intent_kit import IntentContext +IntentGraphBuilder includes validation to ensure: -context = IntentContext() -context.set("user_name", "Alice") -context.set("conversation_history", []) +- No cycles in the graph +- All referenced nodes exist +- All nodes are reachable from root +- Proper node types and relationships -result = graph.route("Hello!", context=context) +```python +# Validate your graph +try: + graph = IntentGraphBuilder().with_json(json_graph).build() + print("Graph is valid!") +except ValueError as e: + print(f"Graph validation failed: {e}") ``` -## Error Handling +### Common Validation Errors -Intent graphs include comprehensive error handling: +- **Missing nodes** - Referenced nodes don't exist +- **Cycles** - Graph contains circular references +- **Unreachable nodes** - Nodes not connected to root +- **Invalid node types** - Incorrect node type specifications -- **Remediation Strategies** - Automatic error recovery -- **Fallback Mechanisms** - Alternative execution paths -- **Error Logging** - Detailed error tracking -- **Graceful Degradation** - Partial success handling +## Advanced Features -## Visualization +### Debug Context -Intent graphs can be visualized for debugging: +Enable debug context to track execution: ```python -# Generate HTML visualization -graph.visualize("graph.html") - -# Enable debug mode -graph.route("Hello Alice", debug=True) +graph = ( + IntentGraphBuilder() + .with_json(json_graph) + .with_functions(function_registry) + .with_debug_context(True) + .build() +) ``` -## LLM Configuration +### Context Tracing -Intent graphs can be configured with LLM settings for intelligent chunk classification: +Enable context tracing for detailed execution logs: ```python -# Set LLM configuration at the graph level -llm_config = { - "provider": "openai", - "model": "gpt-4", - "api_key": "your-api-key" -} - graph = ( IntentGraphBuilder() - .root(classifier) - .with_default_llm_config(llm_config) + .with_json(json_graph) + .with_functions(function_registry) + .with_context_trace(True) .build() ) ``` + +## Best Practices + +### Graph Design + +1. **Keep it simple** - Start with a single root classifier +2. **Use descriptive names** - Make node names clear and meaningful +3. **Group related actions** - Organize actions logically +4. **Test thoroughly** - Validate with various inputs + +### Performance + +1. **Optimize classifiers** - Use efficient classification strategies +2. **Cache results** - Cache expensive operations when possible +3. **Monitor execution** - Track performance metrics +4. **Scale gradually** - Add complexity incrementally + +### Maintenance + +1. **Document your graphs** - Keep JSON configurations well-documented +2. **Version control** - Track changes to graph configurations +3. **Test changes** - Validate modifications before deployment +4. **Monitor usage** - Track how your graphs are being used diff --git a/docs/concepts/nodes-and-actions.md b/docs/concepts/nodes-and-actions.md index 854ff2a..092be4e 100644 --- a/docs/concepts/nodes-and-actions.md +++ b/docs/concepts/nodes-and-actions.md @@ -33,8 +33,7 @@ weather_action = action( name="weather", description="Get weather information for a city", action_func=lambda city: f"Weather in {city} is sunny", - param_schema={"city": str}, - llm_config={"provider": "openai", "model": "gpt-4"} + param_schema={"city": str} ) ``` @@ -44,10 +43,6 @@ weather_action = action( - **description** - Human-readable description - **action_func** - Function to execute - **param_schema** - Parameter type definitions -- **llm_config** - Optional LLM configuration for parameter extraction -- **context_inputs** - Context keys the action reads -- **context_outputs** - Context keys the action writes -- **remediation_strategies** - Error handling strategies ### Classifier Nodes @@ -68,167 +63,196 @@ main_classifier = llm_classifier( ) ``` -#### Keyword Classifier +## Using JSON Configuration -Uses keyword matching for classification: +For more complex workflows, you can define nodes in JSON: ```python -from intent_kit import keyword_classifier +from intent_kit import IntentGraphBuilder -main_classifier = keyword_classifier( - name="main", - description="Route user input to appropriate action", - children=[greet_action, weather_action, calculator_action], - keywords={"greet": ["hello", "hi"], "weather": ["weather", "forecast"]} +# Define your functions +def greet(name, context=None): + return f"Hello {name}!" + +def weather(city, context=None): + return f"Weather in {city} is sunny" + +# Create function registry +function_registry = { + "greet": greet, + "weather": weather, +} + +# Define your graph in JSON +graph_config = { + "root": "main_classifier", + "nodes": { + "main_classifier": { + "id": "main_classifier", + "type": "classifier", + "classifier_type": "llm", + "name": "main_classifier", + "description": "Main intent classifier", + "llm_config": { + "provider": "openai", + "model": "gpt-3.5-turbo", + }, + "children": ["greet_action", "weather_action"], + }, + "greet_action": { + "id": "greet_action", + "type": "action", + "name": "greet_action", + "description": "Greet the user", + "function": "greet", + "param_schema": {"name": "str"}, + }, + "weather_action": { + "id": "weather_action", + "type": "action", + "name": "weather_action", + "description": "Get weather information", + "function": "weather", + "param_schema": {"city": "str"}, + }, + }, +} + +# Build your graph +graph = ( + IntentGraphBuilder() + .with_json(graph_config) + .with_functions(function_registry) + .build() ) ``` - - ## Parameter Extraction ### Automatic Extraction -When `llm_config` is provided, parameters are automatically extracted from natural language: +When using LLM classifiers, parameters are automatically extracted from natural language: ```python # Input: "What's the weather in San Francisco?" # Extracted: {"city": "San Francisco"} -weather_action = action( - name="weather", - action_func=lambda city: f"Weather in {city} is sunny", - param_schema={"city": str}, - llm_config={"provider": "openai", "model": "gpt-4"} -) +# Input: "Hello Alice" +# Extracted: {"name": "Alice"} ``` -### Manual Extraction +### Parameter Schema -Parameters can be extracted manually using regex or other methods: +Define the expected parameters and their types: ```python -import re - -def extract_city(text): - match = re.search(r"weather in (\w+)", text) - return {"city": match.group(1)} if match else {} - -weather_action = action( - name="weather", - action_func=lambda city: f"Weather in {city} is sunny", - param_schema={"city": str}, - param_extractor=extract_city -) +param_schema = { + "name": str, + "age": int, + "city": str, + "temperature": float +} ``` -## Context Integration - -### Reading Context +## Building Graphs -Handlers can read from context: +### Using IntentGraphBuilder ```python -def personalized_greet(name, context): - user_preference = context.get("user_preference", "formal") - if user_preference == "formal": - return f"Good day, {name}!" - else: - return f"Hey {name}!" +from intent_kit import IntentGraphBuilder +from intent_kit.utils.node_factory import action, llm_classifier +# Define actions greet_action = action( name="greet", - action_func=personalized_greet, - param_schema={"name": str}, - context_inputs=["user_preference"] + description="Greet the user", + action_func=lambda name: f"Hello {name}!", + param_schema={"name": str} ) -``` - -### Writing Context -Handlers can write to context: - -```python -def track_conversation(name, context): - history = context.get("conversation_history", []) - history.append(f"Greeted {name}") - context.set("conversation_history", history) - return f"Hello {name}!" +weather_action = action( + name="weather", + description="Get weather information", + action_func=lambda city: f"Weather in {city} is sunny", + param_schema={"city": str} +) -greet_action = action( - name="greet", - action_func=track_conversation, - param_schema={"name": str}, - context_outputs=["conversation_history"] +# Create classifier +main_classifier = llm_classifier( + name="main", + description="Route to appropriate action", + children=[greet_action, weather_action], + llm_config={"provider": "openai", "model": "gpt-4"} ) -``` -## Error Handling +# Build graph +graph = IntentGraphBuilder().root(main_classifier).build() +``` -### Remediation Strategies +### Using JSON Configuration -Handlers can include remediation strategies: +For complex workflows, JSON configuration provides more flexibility: ```python -from intent_kit import RetryStrategy, FallbackStrategy - -weather_action = action( - name="weather", - action_func=get_weather, - param_schema={"city": str}, - remediation_strategies=[ - RetryStrategy(max_retries=3), - FallbackStrategy(fallback_func=lambda: "Weather service unavailable") - ] +# Define your graph in JSON +graph_config = { + "root": "main_classifier", + "nodes": { + "main_classifier": { + "id": "main_classifier", + "type": "classifier", + "classifier_type": "llm", + "name": "main_classifier", + "description": "Main intent classifier", + "llm_config": { + "provider": "openai", + "model": "gpt-4" + }, + "children": ["greet_action", "weather_action"], + }, + "greet_action": { + "id": "greet_action", + "type": "action", + "name": "greet_action", + "description": "Greet the user", + "function": "greet", + "param_schema": {"name": "str"}, + }, + "weather_action": { + "id": "weather_action", + "type": "action", + "name": "weather_action", + "description": "Get weather information", + "function": "weather", + "param_schema": {"city": "str"}, + }, + }, +} + +# Build graph +graph = ( + IntentGraphBuilder() + .with_json(graph_config) + .with_functions(function_registry) + .build() ) ``` -### Error Recovery - -Handlers can recover from errors: +## Testing Your Workflows ```python -def robust_weather(city): - try: - return get_weather_api(city) - except Exception as e: - return f"Weather information for {city} is currently unavailable" +# Test your workflow +result = graph.route("Hello Alice") +print(result.output) # → "Hello Alice!" -weather_action = action( - name="weather", - action_func=robust_weather, - param_schema={"city": str} -) +result = graph.route("What's the weather in San Francisco?") +print(result.output) # → "Weather in San Francisco is sunny" ``` ## Best Practices -### Naming Conventions - -- Use descriptive, lowercase names with underscores -- Prefix classifiers with their type (e.g., `llm_classifier`, `keyword_classifier`) -- Use action-oriented names for actions (e.g., `greet_user`, `get_weather`) - -### Parameter Schemas - -- Define comprehensive parameter schemas -- Use appropriate types (str, int, float, bool, list, dict) -- Include validation where possible - -### Error Handling - -- Always include error handling -- Use appropriate remediation strategies -- Provide meaningful error messages - -### Documentation - -- Write clear descriptions for all nodes -- Document complex parameter extraction logic -- Include usage examples - -### Testing - -- Test actions with various input scenarios -- Test error conditions and edge cases -- Validate parameter extraction accuracy +1. **Keep actions focused** - Each action should do one thing well +2. **Use descriptive names** - Make your action and classifier names clear +3. **Provide good descriptions** - Help the LLM understand what each action does +4. **Test thoroughly** - Use the evaluation framework to test your workflows +5. **Handle errors gracefully** - Make sure your actions can handle unexpected inputs diff --git a/docs/index.md b/docs/index.md index d2198c6..e965b36 100644 --- a/docs/index.md +++ b/docs/index.md @@ -56,8 +56,8 @@ Create systems that make smart decisions based on user requests. ## 🚀 Key Features - **Smart Understanding** - Works with any AI model, extracts parameters automatically -- **Multi-Step Workflows** - Chain actions together, handle complex requests -- **Visualization** - See your workflows as interactive diagrams +- **JSON Configuration** - Define complex workflows in JSON for easy management +- **Function Registry** - Register your functions and use them in actions - **Developer Friendly** - Simple API, comprehensive error handling, built-in debugging - **Testing & Evaluation** - Test against real datasets, measure performance diff --git a/docs/quickstart.md b/docs/quickstart.md index e397b5d..24742e0 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -33,6 +33,7 @@ greet_action = action( # Create a classifier to understand user requests classifier = llm_classifier( name="main", + description="Route to appropriate action", children=[greet_action], llm_config={"provider": "openai", "model": "gpt-3.5-turbo"} ) @@ -52,6 +53,76 @@ print(result.output) # → "Hello Alice!" 3. **We built a graph** - This connects everything together 4. **We tested it** - The bot understood "Hello Alice" and extracted the name "Alice" +## Using JSON Configuration + +For more complex workflows, you can define your graph in JSON: + +```python +from intent_kit import IntentGraphBuilder + +# Define your functions +def greet(name, context=None): + return f"Hello {name}!" + +def calculate(operation, a, b, context=None): + if operation == "add": + return a + b + return None + +# Create function registry +function_registry = { + "greet": greet, + "calculate": calculate, +} + +# Define your graph in JSON +graph_config = { + "root": "main_classifier", + "nodes": { + "main_classifier": { + "id": "main_classifier", + "type": "classifier", + "classifier_type": "llm", + "name": "main_classifier", + "description": "Main intent classifier", + "llm_config": { + "provider": "openai", + "model": "gpt-3.5-turbo", + }, + "children": ["greet_action", "calculate_action"], + }, + "greet_action": { + "id": "greet_action", + "type": "action", + "name": "greet_action", + "description": "Greet the user", + "function": "greet", + "param_schema": {"name": "str"}, + }, + "calculate_action": { + "id": "calculate_action", + "type": "action", + "name": "calculate_action", + "description": "Perform a calculation", + "function": "calculate", + "param_schema": {"operation": "str", "a": "float", "b": "float"}, + }, + }, +} + +# Build your graph +graph = ( + IntentGraphBuilder() + .with_json(graph_config) + .with_functions(function_registry) + .build() +) + +# Test it! +result = graph.route("Hello Alice") +print(result.output) # → "Hello Alice!" +``` + ## Try More Examples ```python @@ -61,6 +132,10 @@ print(result.output) # → "Hello Bob!" result = graph.route("Greet Sarah") print(result.output) # → "Hello Sarah!" + +# Test calculations +result = graph.route("Add 5 and 3") +print(result.output) # → 8 ``` ## Next Steps diff --git a/examples/basic/simple_demo.py b/examples/basic/simple_demo.py index d37c378..83f6817 100644 --- a/examples/basic/simple_demo.py +++ b/examples/basic/simple_demo.py @@ -1,56 +1,60 @@ """ -Simple IntentGraph Demo with Reporting +Simple IntentGraph Demo -A minimal demonstration showing how to configure an intent graph with actions and classifiers, -using the new reporting functionality. +A minimal demonstration showing how to configure an intent graph with actions and classifiers. +This example shows both the programmatic API and JSON configuration approaches. """ import os from dotenv import load_dotenv from intent_kit import IntentGraphBuilder +from intent_kit import action, llm_classifier from intent_kit.utils.perf_util import PerfUtil from intent_kit.utils.report_utils import ReportUtil from typing import Dict, Callable, Any, List, Tuple load_dotenv() +# LLM Configuration LLM_CONFIG = { "provider": "openrouter", "api_key": os.getenv("OPENROUTER_API_KEY"), "model": "mistralai/ministral-8b", } +# Define action functions + def greet(name, context=None): + """Greet a user by name.""" return f"Hello {name}!" def calculate(operation, a, b, context=None): - # Simple operation mapping + """Perform a simple calculation.""" operation = operation.lower() - if operation == "plus": + if operation in ["plus", "add"]: return a + b - if operation == "minus": + elif operation in ["minus", "subtract"]: return a - b - if operation == "times": + elif operation in ["times", "multiply"]: return a * b - if operation == "divided": + elif operation in ["divided", "divide"]: return a / b - if operation == "add": - return a + b - if operation == "multiply": - return a * b return None def weather(location, context=None): + """Get weather information for a location.""" return f"Weather in {location}: 72°F, Sunny (simulated)" def help_action(context=None): + """Provide help information.""" return "I can help with greetings, calculations, and weather!" +# Create function registry function_registry: Dict[str, Callable[..., Any]] = { "greet": greet, "calculate": calculate, @@ -58,6 +62,7 @@ def help_action(context=None): "help_action": help_action, } +# JSON configuration for the graph simple_demo_graph = { "root": "main_classifier", "nodes": { @@ -115,37 +120,114 @@ def help_action(context=None): }, } -if __name__ == "__main__": + +def demonstrate_programmatic_api(): + """Demonstrate building a graph using the programmatic API.""" + print("=== Programmatic API Demo ===") + + # Define actions using the node factory + greet_action = action( + name="greet", + description="Greet the user by name", + action_func=lambda name: f"Hello {name}!", + param_schema={"name": str}, + ) + + # Create classifier + classifier = llm_classifier( + name="main", + description="Route to appropriate action", + children=[greet_action], + llm_config=LLM_CONFIG, + ) + + # Build graph + graph = IntentGraphBuilder().root(classifier).build() + + # Test it + result = graph.route("Hello Alice") + print("Input: 'Hello Alice'") + print(f"Output: {result.output}") + print() + + +def demonstrate_json_configuration(): + """Demonstrate building a graph using JSON configuration.""" + print("=== JSON Configuration Demo ===") + + # Build graph from JSON + graph = ( + IntentGraphBuilder() + .with_json(simple_demo_graph) + .with_functions(function_registry) + .with_default_llm_config(LLM_CONFIG) + .build() + ) + + # Test inputs + test_inputs = [ + "Hello, my name is Alice", + "What's 15 plus 7?", + "Weather in San Francisco", + "Help me", + "Multiply 8 and 3", + ] + + print("Testing various inputs:") + for test_input in test_inputs: + result = graph.route(test_input) + print(f"Input: '{test_input}'") + print(f"Output: {result.output}") + print() + + +def demonstrate_performance_tracking(): + """Demonstrate performance tracking and reporting.""" + print("=== Performance Tracking Demo ===") + + graph = ( + IntentGraphBuilder() + .with_json(simple_demo_graph) + .with_functions(function_registry) + .with_default_llm_config(LLM_CONFIG) + .build() + ) + + test_inputs = [ + "Hello, my name is Alice", + "What's 15 plus 7?", + "Weather in San Francisco", + "Help me", + "Multiply 8 and 3", + ] + + results = [] + timings: List[Tuple[str, float]] = [] + with PerfUtil("simple_demo.py run time") as perf: - graph = ( - IntentGraphBuilder() - .with_json(simple_demo_graph) - .with_functions(function_registry) - .with_default_llm_config(LLM_CONFIG) - .build() - ) - - test_inputs = [ - "Hello, my name is Alice", - "What's 15 plus 7?", - "Weather in San Francisco", - "Help me", - "Multiply 8 and 3", - ] - - results = [] - timings: List[Tuple[str, float]] = [] for test_input in test_inputs: with PerfUtil.collect(test_input, timings) as perf: result = graph.route(test_input) results.append(result) - # Use the new format_execution_results method to format the existing results - report = ReportUtil.format_execution_results( - results=results, - llm_config=LLM_CONFIG, - perf_info=perf.format(), - timings=timings, - ) + # Generate performance report + report = ReportUtil.format_execution_results( + results=results, + llm_config=LLM_CONFIG, + perf_info=perf.format(), + timings=timings, + ) + + print("Performance Report:") + print(report) - print(report) + +if __name__ == "__main__": + print("Intent Kit Simple Demo") + print("=" * 50) + print() + + # Demonstrate different approaches + demonstrate_programmatic_api() + demonstrate_json_configuration() + demonstrate_performance_tracking() diff --git a/intent_kit/__init__.py b/intent_kit/__init__.py index 15faff3..8f8c05d 100644 --- a/intent_kit/__init__.py +++ b/intent_kit/__init__.py @@ -16,8 +16,8 @@ from .graph.builder import IntentGraphBuilder from .context import IntentContext -# For advanced node helpers (llm_classifier, llm_splitter, etc.), -# import directly from intent_kit.utils.node_factory in your code. +# Export node factory functions for easier access +from .utils.node_factory import action, llm_classifier __version__ = "0.5.0" @@ -28,4 +28,6 @@ "ClassifierNode", "ActionNode", "IntentContext", + "action", + "llm_classifier", ] diff --git a/scripts/validate_codecov.py b/scripts/validate_codecov.py new file mode 100755 index 0000000..9a88d5c --- /dev/null +++ b/scripts/validate_codecov.py @@ -0,0 +1,262 @@ +#!/usr/bin/env python3 +""" +Validate codecov.yml against actual directory structure. + +This script checks that all paths referenced in codecov.yml actually exist +in the filesystem, and reports any missing directories or files. +""" + +import os +import sys +import yaml +import subprocess +import json +from pathlib import Path +from typing import List, Set + + +def get_actual_directory_structure() -> Set[str]: + """Get the actual directory structure using 'tree' command.""" + try: + # Run tree command to get directory structure + result = subprocess.run( + ["tree", "-I", "*.pyc|htmlcov|site|dist", "-f"], + capture_output=True, + text=True, + cwd=Path(__file__).parent.parent, + ) + + if result.returncode != 0: + print(f"Error running tree command: {result.stderr}") + return set() + + # Parse tree output to get file paths + paths = set() + for line in result.stdout.split("\n"): + if ( + line.strip() + and not line.startswith("└──") + and not line.startswith("├──") + ): + # Extract the file path from tree output + parts = line.split("── ") + if len(parts) > 1: + path = parts[1].strip() + if path and not path.endswith("/"): + paths.add(path) + + return paths + + except FileNotFoundError: + print("Error: 'tree' command not found. Please install tree.") + return set() + except Exception as e: + print(f"Error getting directory structure: {e}") + return set() + + +def get_codecov_paths() -> Set[str]: + """Extract all paths from codecov.yml file.""" + codecov_file = Path(__file__).parent.parent / ".codecov.yml" + + if not codecov_file.exists(): + print(f"Error: {codecov_file} not found") + return set() + + try: + with open(codecov_file, "r") as f: + config = yaml.safe_load(f) + + paths = set() + + # Extract paths from component_management.individual_components + if "component_management" in config: + components = config["component_management"].get("individual_components", []) + for component in components: + component_paths = component.get("paths", []) + for path in component_paths: + # Convert glob patterns to actual paths + if "**" in path: + # Handle directory globs + base_path = path.replace("/**", "") + if os.path.exists(base_path): + for root, dirs, files in os.walk(base_path): + for file in files: + if file.endswith(".py"): + paths.add(os.path.join(root, file)) + elif path.endswith("**"): + # Handle directory globs + base_path = path[:-2] + if os.path.exists(base_path): + for root, dirs, files in os.walk(base_path): + for file in files: + if file.endswith(".py"): + paths.add(os.path.join(root, file)) + elif path.endswith(".py"): + # Handle specific Python files + if os.path.exists(path): + paths.add(path) + else: + # Handle specific directories + if os.path.exists(path): + for root, dirs, files in os.walk(path): + for file in files: + if file.endswith(".py"): + paths.add(os.path.join(root, file)) + + return paths + + except yaml.YAMLError as e: + print(f"Error parsing codecov.yml: {e}") + return set() + except Exception as e: + print(f"Error reading codecov.yml: {e}") + return set() + + +def validate_codecov_online() -> bool: + """Validate codecov.yml using the online validator.""" + codecov_file = Path(__file__).parent.parent / ".codecov.yml" + + if not codecov_file.exists(): + print(f"Error: {codecov_file} not found") + return False + + try: + with open(codecov_file, "r") as f: + content = f.read() + result = subprocess.run( + ["curl", "--data-binary", "@-", "https://codecov.io/validate"], + input=content, + capture_output=True, + text=True, + ) + + if result.returncode == 0: + # Remove "Valid!" prefix if present + stdout = result.stdout.strip() + if stdout.startswith("Valid!"): + stdout = stdout[6:].strip() + + try: + response = json.loads(stdout) + if "component_management" in response: + print("✅ codecov.yml is valid according to online validator") + return True + else: + print("❌ codecov.yml is invalid according to online validator") + return False + except json.JSONDecodeError: + print( + f"❌ Invalid JSON response from online validator: {result.stdout}" + ) + return False + else: + print(f"❌ Error calling online validator: {result.stderr}") + return False + + except Exception as e: + print(f"❌ Error validating codecov.yml online: {e}") + return False + + +def check_missing_paths(codecov_paths: Set[str], actual_paths: Set[str]) -> List[str]: + """Check for paths in codecov.yml that don't exist in the filesystem.""" + missing = [] + + for path in codecov_paths: + if path not in actual_paths: + # Check if it's a directory that should exist + if path.endswith("/**") or path.endswith("**"): + base_path = path.replace("/**", "").replace("**", "") + if not os.path.exists(base_path): + missing.append(f"Directory does not exist: {base_path}") + elif path.endswith(".py"): + if not os.path.exists(path): + missing.append(f"File does not exist: {path}") + else: + if not os.path.exists(path): + missing.append(f"Path does not exist: {path}") + + return missing + + +def check_uncovered_paths(codecov_paths: Set[str], actual_paths: Set[str]) -> List[str]: + """Check for Python files in the filesystem that aren't covered by codecov.yml.""" + uncovered = [] + + for path in actual_paths: + if path.endswith(".py") and path not in codecov_paths: + # Skip some common directories that might not need coverage + skip_dirs = {"__pycache__", "tests", "examples", "scripts"} + if not any(skip_dir in path for skip_dir in skip_dirs): + uncovered.append(path) + + return uncovered + + +def main(): + """Main validation function.""" + print("🔍 Validating codecov.yml against directory structure...") + print() + + # Get actual directory structure + print("📁 Scanning directory structure...") + actual_paths = get_actual_directory_structure() + print(f"Found {len(actual_paths)} files in directory structure") + + # Get codecov paths + print("📋 Extracting paths from codecov.yml...") + codecov_paths = get_codecov_paths() + print(f"Found {len(codecov_paths)} paths in codecov.yml") + + # Validate online + print("🌐 Validating codecov.yml online...") + online_valid = validate_codecov_online() + + # Check for missing paths + print("🔍 Checking for missing paths...") + missing_paths = check_missing_paths(codecov_paths, actual_paths) + + # Check for uncovered paths + print("🔍 Checking for uncovered paths...") + uncovered_paths = check_uncovered_paths(codecov_paths, actual_paths) + + # Report results + print() + print("=" * 60) + print("VALIDATION RESULTS") + print("=" * 60) + + if missing_paths: + print("❌ MISSING PATHS (referenced in codecov.yml but don't exist):") + for path in missing_paths: + print(f" - {path}") + print() + else: + print("✅ All paths in codecov.yml exist in filesystem") + + if uncovered_paths: + print("⚠️ UNCOVERED PATHS (exist in filesystem but not in codecov.yml):") + for path in uncovered_paths[:10]: # Show first 10 + print(f" - {path}") + if len(uncovered_paths) > 10: + print(f" ... and {len(uncovered_paths) - 10} more") + print() + else: + print("✅ All Python files are covered by codecov.yml") + + # Overall result + if missing_paths: + print("❌ VALIDATION FAILED: Missing paths found") + return 1 + elif not online_valid: + print("❌ VALIDATION FAILED: Online validation failed") + return 1 + else: + print("✅ VALIDATION PASSED: All checks passed") + return 0 + + +if __name__ == "__main__": + sys.exit(main())