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 @@
Build reliable, auditable AI applications that understand user intent and take intelligent actions
-
-
-
+
-
---
## 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())