functualize-ai 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- functualize_ai-0.1.0/.gitignore +101 -0
- functualize_ai-0.1.0/PKG-INFO +134 -0
- functualize_ai-0.1.0/README.md +114 -0
- functualize_ai-0.1.0/examples/README.md +13 -0
- functualize_ai-0.1.0/examples/summarize/summarize.py +48 -0
- functualize_ai-0.1.0/examples/summarize/test_summarize.py +16 -0
- functualize_ai-0.1.0/pyproject.toml +38 -0
- functualize_ai-0.1.0/src/functualize_ai/__init__.py +150 -0
- functualize_ai-0.1.0/src/functualize_ai/_ai.py +436 -0
- functualize_ai-0.1.0/src/functualize_ai/_budget.py +150 -0
- functualize_ai-0.1.0/src/functualize_ai/_config.py +15 -0
- functualize_ai-0.1.0/src/functualize_ai/_errors.py +19 -0
- functualize_ai-0.1.0/src/functualize_ai/_events.py +7 -0
- functualize_ai-0.1.0/src/functualize_ai/_gate_strategy.py +141 -0
- functualize_ai-0.1.0/src/functualize_ai/_metadata.py +37 -0
- functualize_ai-0.1.0/src/functualize_ai/_protocols.py +83 -0
- functualize_ai-0.1.0/src/functualize_ai/_provider_discovery.py +207 -0
- functualize_ai-0.1.0/src/functualize_ai/_state_fallback.py +136 -0
- functualize_ai-0.1.0/src/functualize_ai/_tool_scope.py +464 -0
- functualize_ai-0.1.0/src/functualize_ai/_types.py +59 -0
- functualize_ai-0.1.0/src/functualize_ai/py.typed +0 -0
- functualize_ai-0.1.0/src/functualize_ai/testing/__init__.py +13 -0
- functualize_ai-0.1.0/src/functualize_ai/testing/_mock_ai.py +223 -0
- functualize_ai-0.1.0/tests/__init__.py +0 -0
- functualize_ai-0.1.0/tests/conftest.py +1 -0
- functualize_ai-0.1.0/tests/test_ai_capability.py +263 -0
- functualize_ai-0.1.0/tests/test_plugin.py +22 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
*.egg-info/
|
|
7
|
+
*.egg
|
|
8
|
+
dist/
|
|
9
|
+
build/
|
|
10
|
+
*.whl
|
|
11
|
+
|
|
12
|
+
# Agents
|
|
13
|
+
.spec/archive/
|
|
14
|
+
.spec/features/
|
|
15
|
+
.spec/scrutiny-reports/
|
|
16
|
+
.spec/.agentic-coding
|
|
17
|
+
.spec/STATE.md
|
|
18
|
+
.spec/PROJECT.md
|
|
19
|
+
.spec/REQUIREMENTS.md
|
|
20
|
+
.spec/ROADMAP.md
|
|
21
|
+
.opencode/
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Virtual environments
|
|
25
|
+
.venv/
|
|
26
|
+
venv/
|
|
27
|
+
ENV/
|
|
28
|
+
|
|
29
|
+
# Testing
|
|
30
|
+
.coverage
|
|
31
|
+
.pytest_cache/
|
|
32
|
+
htmlcov/
|
|
33
|
+
.hypothesis/
|
|
34
|
+
snapshot_report.html
|
|
35
|
+
_*_result*.txt
|
|
36
|
+
_debug.txt
|
|
37
|
+
_tui_debug.txt
|
|
38
|
+
_tui_eval_debug.txt
|
|
39
|
+
|
|
40
|
+
# IDE
|
|
41
|
+
.idea/
|
|
42
|
+
*.swp
|
|
43
|
+
*.swo
|
|
44
|
+
*~
|
|
45
|
+
*.code-workspace
|
|
46
|
+
|
|
47
|
+
# Coding-agent tooling state (guards — these dirs are not part of the repo)
|
|
48
|
+
.kiro/
|
|
49
|
+
.moai/
|
|
50
|
+
|
|
51
|
+
# OS
|
|
52
|
+
.DS_Store
|
|
53
|
+
Thumbs.db
|
|
54
|
+
|
|
55
|
+
# Environment / secrets
|
|
56
|
+
.env
|
|
57
|
+
.env.*
|
|
58
|
+
!.env.example
|
|
59
|
+
|
|
60
|
+
# Agent scratch space (test output, temp scripts)
|
|
61
|
+
tmp/
|
|
62
|
+
|
|
63
|
+
# Local-only files (not for the repo)
|
|
64
|
+
*.local.md
|
|
65
|
+
*.local.*
|
|
66
|
+
|
|
67
|
+
# Personal notes
|
|
68
|
+
HUMAN_NOTE.md
|
|
69
|
+
|
|
70
|
+
# Distribution
|
|
71
|
+
dist/
|
|
72
|
+
|
|
73
|
+
# Documentation site build output
|
|
74
|
+
site/
|
|
75
|
+
|
|
76
|
+
# uv
|
|
77
|
+
.python-version
|
|
78
|
+
.functualize/cache.json
|
|
79
|
+
.functualize_cache.json
|
|
80
|
+
.todos/
|
|
81
|
+
.sidecar/
|
|
82
|
+
.sidecar-agent
|
|
83
|
+
.sidecar-task
|
|
84
|
+
.sidecar-pr
|
|
85
|
+
.sidecar-start.sh
|
|
86
|
+
.sidecar-base
|
|
87
|
+
.td-root
|
|
88
|
+
.functualize/
|
|
89
|
+
.import_linter_cache/
|
|
90
|
+
.mypy_cache/
|
|
91
|
+
.pytest_cache/
|
|
92
|
+
.ruff_cache/
|
|
93
|
+
|
|
94
|
+
# OmO / OpenCode agent run-continuation scratch state
|
|
95
|
+
.omo/
|
|
96
|
+
.mcp.json
|
|
97
|
+
.agentsroom/handoff-transcript-*.txt
|
|
98
|
+
.agentsroom/handoff-summary-*.md
|
|
99
|
+
|
|
100
|
+
# Internal pre-release audit reports (contain session IDs / local infra notes)
|
|
101
|
+
.release/
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: functualize-ai
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AI Domain SDK for functualize — LLM interaction capabilities
|
|
5
|
+
Author-email: Mohammad Hakim Adiprasetya <viltohmyst@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Typing :: Typed
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
Requires-Dist: functualize-state
|
|
15
|
+
Requires-Dist: pydantic>=2.0.0
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
|
|
18
|
+
Requires-Dist: pytest>=7.4.0; extra == 'dev'
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# functualize-ai
|
|
22
|
+
|
|
23
|
+
> **Status: Published** — Independently installable from PyPI.
|
|
24
|
+
|
|
25
|
+
AI Domain SDK for functualize — provider-agnostic LLM interaction capabilities.
|
|
26
|
+
|
|
27
|
+
Provides the `AI` capability class with structured output, tool calling, streaming,
|
|
28
|
+
and extraction methods backed by a pluggable `AIProvider` protocol. Includes
|
|
29
|
+
deny-by-default tool visibility via `ToolScope`, cumulative budget enforcement,
|
|
30
|
+
lifecycle event emission, and a deterministic `MockAI` testing double for unit
|
|
31
|
+
testing without network calls or API keys.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install functualize-ai
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from functualize_ai import AI, ToolScope, AILimits
|
|
43
|
+
from functualize_ai.testing import MockAI
|
|
44
|
+
|
|
45
|
+
# Use MockAI for deterministic testing (no API key needed)
|
|
46
|
+
ai = MockAI(responses={"*summarize*": "A brief summary of the document."})
|
|
47
|
+
|
|
48
|
+
# Simple text completion
|
|
49
|
+
result = ai.complete("Please summarize this text")
|
|
50
|
+
print(result) # "A brief summary of the document."
|
|
51
|
+
|
|
52
|
+
# Run with tool scope and budget limits
|
|
53
|
+
scope = ToolScope.only(["search", "calculate"])
|
|
54
|
+
limits = AILimits(budget_usd=1.00, max_tool_calls=5)
|
|
55
|
+
ai_result = ai.run("Find the answer", tools=scope, limits=limits)
|
|
56
|
+
print(ai_result.output)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Features
|
|
60
|
+
|
|
61
|
+
- **Provider-agnostic AI class** with `complete()`, `run()`, `stream()`, and `extract()` methods for text, structured output, tool calling, and streaming
|
|
62
|
+
- **ToolScope builder** implementing deny-by-default tool visibility — restrict tools by name, tag, group, or plain callables with composable `+` operator
|
|
63
|
+
- **Budget enforcement** tracking cumulative USD spend across calls with automatic `BudgetExceeded` errors when limits are reached
|
|
64
|
+
- **Structured output validation** with automatic retry (up to 3 attempts) against Pydantic models or dataclasses
|
|
65
|
+
- **MockAI testing double** using glob-pattern matching for deterministic, network-free testing with full call recording
|
|
66
|
+
- **Lifecycle event emission** (`AI_CALL_STARTED`, `AI_CALL_COMPLETED`, `AI_CALL_FAILED`, `AI_BUDGET_EXCEEDED`, `AI_TOOL_CALLED`) for observability and audit logging
|
|
67
|
+
- **AIProvider protocol** enabling custom backend implementations (PydanticAI, LiteLLM, or any LLM SDK)
|
|
68
|
+
|
|
69
|
+
## API Reference
|
|
70
|
+
|
|
71
|
+
Public classes and functions exported by this plugin:
|
|
72
|
+
|
|
73
|
+
### Capability
|
|
74
|
+
|
|
75
|
+
- `AI` — Provider-agnostic LLM interaction class with `complete()`, `run()`, `stream()`, `extract()` methods
|
|
76
|
+
|
|
77
|
+
### Protocol
|
|
78
|
+
|
|
79
|
+
- `AIProvider` — Runtime-checkable protocol that AI backend implementations must satisfy
|
|
80
|
+
|
|
81
|
+
### Tool Scope
|
|
82
|
+
|
|
83
|
+
- `ToolScope` — Deny-by-default tool visibility builder with `only()`, `tagged()`, `group()`, `functions()` factory methods
|
|
84
|
+
|
|
85
|
+
### Types
|
|
86
|
+
|
|
87
|
+
- `AIResult` — Result of an AI run containing `output`, `tool_calls`, `usage`, and `duration_ms`
|
|
88
|
+
- `TokenUsage` — Token usage statistics (`prompt_tokens`, `completion_tokens`, `total_tokens`, `cost_usd`)
|
|
89
|
+
- `ToolDef` — Provider-agnostic tool definition with name, description, and parameters schema
|
|
90
|
+
- `AILimits` — Budget and constraint caps (`max_tool_calls`, `max_tokens`, `budget_usd`, `timeout_seconds`)
|
|
91
|
+
- `ToolCallRecord` — Record of a single tool call with name, args, result, and duration
|
|
92
|
+
|
|
93
|
+
### Configuration
|
|
94
|
+
|
|
95
|
+
- `AIConfig` — Pydantic model for AI domain configuration (provider, model, max_tokens, budget_usd, timeout_seconds)
|
|
96
|
+
|
|
97
|
+
### Errors
|
|
98
|
+
|
|
99
|
+
- `AINotAvailable` — Raised when no AI provider is configured
|
|
100
|
+
- `BudgetExceeded` — Raised when cumulative spend reaches the budget limit
|
|
101
|
+
- `ToolNotPermitted` — Raised when a tool call is not permitted by the current ToolScope
|
|
102
|
+
|
|
103
|
+
### Events
|
|
104
|
+
|
|
105
|
+
- `AI_CALL_STARTED` — Emitted when an AI call begins
|
|
106
|
+
- `AI_CALL_COMPLETED` — Emitted when an AI call completes successfully
|
|
107
|
+
- `AI_CALL_FAILED` — Emitted when an AI call fails
|
|
108
|
+
- `AI_BUDGET_EXCEEDED` — Emitted when budget is exceeded
|
|
109
|
+
- `AI_TOOL_CALLED` — Emitted when a tool is called during a run
|
|
110
|
+
|
|
111
|
+
### Testing
|
|
112
|
+
|
|
113
|
+
- `MockAI` — Pattern-matching AI mock for deterministic testing
|
|
114
|
+
- `MockAICall` — Record of a single MockAI call (prompt, response_model, response)
|
|
115
|
+
|
|
116
|
+
### Provider Discovery
|
|
117
|
+
|
|
118
|
+
- `discover_ai_providers()` — Discover available AI provider entry points
|
|
119
|
+
- `select_ai_provider()` — Select a provider by name from discovered providers
|
|
120
|
+
- `resolve_ai_provider()` — Resolve and instantiate the configured AI provider
|
|
121
|
+
|
|
122
|
+
### State Fallback
|
|
123
|
+
|
|
124
|
+
- `EphemeralStateBackend` — In-memory state backend for when no persistent state is available
|
|
125
|
+
- `StrictStateBackendWrapper` — Wrapper enforcing strict key access on a state backend
|
|
126
|
+
- `resolve_ai_state_backend()` — Resolve the AI state backend from context
|
|
127
|
+
|
|
128
|
+
## Development
|
|
129
|
+
|
|
130
|
+
Run plugin tests:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
uv run pytest plugins/functualize-ai/tests/ -v
|
|
134
|
+
```
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# functualize-ai
|
|
2
|
+
|
|
3
|
+
> **Status: Published** — Independently installable from PyPI.
|
|
4
|
+
|
|
5
|
+
AI Domain SDK for functualize — provider-agnostic LLM interaction capabilities.
|
|
6
|
+
|
|
7
|
+
Provides the `AI` capability class with structured output, tool calling, streaming,
|
|
8
|
+
and extraction methods backed by a pluggable `AIProvider` protocol. Includes
|
|
9
|
+
deny-by-default tool visibility via `ToolScope`, cumulative budget enforcement,
|
|
10
|
+
lifecycle event emission, and a deterministic `MockAI` testing double for unit
|
|
11
|
+
testing without network calls or API keys.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install functualize-ai
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from functualize_ai import AI, ToolScope, AILimits
|
|
23
|
+
from functualize_ai.testing import MockAI
|
|
24
|
+
|
|
25
|
+
# Use MockAI for deterministic testing (no API key needed)
|
|
26
|
+
ai = MockAI(responses={"*summarize*": "A brief summary of the document."})
|
|
27
|
+
|
|
28
|
+
# Simple text completion
|
|
29
|
+
result = ai.complete("Please summarize this text")
|
|
30
|
+
print(result) # "A brief summary of the document."
|
|
31
|
+
|
|
32
|
+
# Run with tool scope and budget limits
|
|
33
|
+
scope = ToolScope.only(["search", "calculate"])
|
|
34
|
+
limits = AILimits(budget_usd=1.00, max_tool_calls=5)
|
|
35
|
+
ai_result = ai.run("Find the answer", tools=scope, limits=limits)
|
|
36
|
+
print(ai_result.output)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Features
|
|
40
|
+
|
|
41
|
+
- **Provider-agnostic AI class** with `complete()`, `run()`, `stream()`, and `extract()` methods for text, structured output, tool calling, and streaming
|
|
42
|
+
- **ToolScope builder** implementing deny-by-default tool visibility — restrict tools by name, tag, group, or plain callables with composable `+` operator
|
|
43
|
+
- **Budget enforcement** tracking cumulative USD spend across calls with automatic `BudgetExceeded` errors when limits are reached
|
|
44
|
+
- **Structured output validation** with automatic retry (up to 3 attempts) against Pydantic models or dataclasses
|
|
45
|
+
- **MockAI testing double** using glob-pattern matching for deterministic, network-free testing with full call recording
|
|
46
|
+
- **Lifecycle event emission** (`AI_CALL_STARTED`, `AI_CALL_COMPLETED`, `AI_CALL_FAILED`, `AI_BUDGET_EXCEEDED`, `AI_TOOL_CALLED`) for observability and audit logging
|
|
47
|
+
- **AIProvider protocol** enabling custom backend implementations (PydanticAI, LiteLLM, or any LLM SDK)
|
|
48
|
+
|
|
49
|
+
## API Reference
|
|
50
|
+
|
|
51
|
+
Public classes and functions exported by this plugin:
|
|
52
|
+
|
|
53
|
+
### Capability
|
|
54
|
+
|
|
55
|
+
- `AI` — Provider-agnostic LLM interaction class with `complete()`, `run()`, `stream()`, `extract()` methods
|
|
56
|
+
|
|
57
|
+
### Protocol
|
|
58
|
+
|
|
59
|
+
- `AIProvider` — Runtime-checkable protocol that AI backend implementations must satisfy
|
|
60
|
+
|
|
61
|
+
### Tool Scope
|
|
62
|
+
|
|
63
|
+
- `ToolScope` — Deny-by-default tool visibility builder with `only()`, `tagged()`, `group()`, `functions()` factory methods
|
|
64
|
+
|
|
65
|
+
### Types
|
|
66
|
+
|
|
67
|
+
- `AIResult` — Result of an AI run containing `output`, `tool_calls`, `usage`, and `duration_ms`
|
|
68
|
+
- `TokenUsage` — Token usage statistics (`prompt_tokens`, `completion_tokens`, `total_tokens`, `cost_usd`)
|
|
69
|
+
- `ToolDef` — Provider-agnostic tool definition with name, description, and parameters schema
|
|
70
|
+
- `AILimits` — Budget and constraint caps (`max_tool_calls`, `max_tokens`, `budget_usd`, `timeout_seconds`)
|
|
71
|
+
- `ToolCallRecord` — Record of a single tool call with name, args, result, and duration
|
|
72
|
+
|
|
73
|
+
### Configuration
|
|
74
|
+
|
|
75
|
+
- `AIConfig` — Pydantic model for AI domain configuration (provider, model, max_tokens, budget_usd, timeout_seconds)
|
|
76
|
+
|
|
77
|
+
### Errors
|
|
78
|
+
|
|
79
|
+
- `AINotAvailable` — Raised when no AI provider is configured
|
|
80
|
+
- `BudgetExceeded` — Raised when cumulative spend reaches the budget limit
|
|
81
|
+
- `ToolNotPermitted` — Raised when a tool call is not permitted by the current ToolScope
|
|
82
|
+
|
|
83
|
+
### Events
|
|
84
|
+
|
|
85
|
+
- `AI_CALL_STARTED` — Emitted when an AI call begins
|
|
86
|
+
- `AI_CALL_COMPLETED` — Emitted when an AI call completes successfully
|
|
87
|
+
- `AI_CALL_FAILED` — Emitted when an AI call fails
|
|
88
|
+
- `AI_BUDGET_EXCEEDED` — Emitted when budget is exceeded
|
|
89
|
+
- `AI_TOOL_CALLED` — Emitted when a tool is called during a run
|
|
90
|
+
|
|
91
|
+
### Testing
|
|
92
|
+
|
|
93
|
+
- `MockAI` — Pattern-matching AI mock for deterministic testing
|
|
94
|
+
- `MockAICall` — Record of a single MockAI call (prompt, response_model, response)
|
|
95
|
+
|
|
96
|
+
### Provider Discovery
|
|
97
|
+
|
|
98
|
+
- `discover_ai_providers()` — Discover available AI provider entry points
|
|
99
|
+
- `select_ai_provider()` — Select a provider by name from discovered providers
|
|
100
|
+
- `resolve_ai_provider()` — Resolve and instantiate the configured AI provider
|
|
101
|
+
|
|
102
|
+
### State Fallback
|
|
103
|
+
|
|
104
|
+
- `EphemeralStateBackend` — In-memory state backend for when no persistent state is available
|
|
105
|
+
- `StrictStateBackendWrapper` — Wrapper enforcing strict key access on a state backend
|
|
106
|
+
- `resolve_ai_state_backend()` — Resolve the AI state backend from context
|
|
107
|
+
|
|
108
|
+
## Development
|
|
109
|
+
|
|
110
|
+
Run plugin tests:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
uv run pytest plugins/functualize-ai/tests/ -v
|
|
114
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# functualize-ai Examples
|
|
2
|
+
|
|
3
|
+
The AI domain SDK: provider-agnostic `AI` capability with structured output, `ToolScope`, `AILimits`, and the `MockAI` testing double.
|
|
4
|
+
|
|
5
|
+
| Directory | Demonstrates |
|
|
6
|
+
|-----------|--------------|
|
|
7
|
+
| [`summarize/`](summarize/) | A job calling `ai.complete()` with a Pydantic `response_model`, runnable without API keys via `MockAI` |
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
uv run pytest plugins/functualize-ai/examples/ -v
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
For a real LLM backend, see [`functualize-ai-pydantic/examples/`](../../functualize-ai-pydantic/examples/).
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Provider-agnostic AI job: structured summary of a text.
|
|
2
|
+
|
|
3
|
+
Run with:
|
|
4
|
+
func summarize.py run --text "Functualize is a CLI framework..."
|
|
5
|
+
|
|
6
|
+
Uses MockAI so it works without API keys; with a provider plugin
|
|
7
|
+
installed (e.g. functualize-ai-pydantic) declare `ai: AI` as a job
|
|
8
|
+
parameter instead and the framework injects the real provider.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from functualize_ai.testing import MockAI
|
|
12
|
+
from pydantic import BaseModel, Field
|
|
13
|
+
|
|
14
|
+
from functualize.job import RunContext
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SummarizeConfig(BaseModel):
|
|
18
|
+
"""Configuration for the summarize job."""
|
|
19
|
+
|
|
20
|
+
text: str = Field(description="Text to summarize")
|
|
21
|
+
max_points: int = Field(default=3, ge=1, le=10, description="Bullet points")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Summary(BaseModel):
|
|
25
|
+
"""Structured output the AI must produce."""
|
|
26
|
+
|
|
27
|
+
title: str
|
|
28
|
+
bullet_points: list[str]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def run(config: SummarizeConfig, rc: RunContext) -> Summary:
|
|
32
|
+
"""Summarize text into structured bullet points."""
|
|
33
|
+
ai = MockAI(
|
|
34
|
+
responses={
|
|
35
|
+
"*Summarize*": Summary(
|
|
36
|
+
title="Summary",
|
|
37
|
+
bullet_points=[f"Point about: {config.text[:40]}"] * config.max_points,
|
|
38
|
+
),
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
summary = ai.complete(
|
|
43
|
+
f"Summarize in {config.max_points} bullet points: {config.text}",
|
|
44
|
+
response_model=Summary,
|
|
45
|
+
)
|
|
46
|
+
for point in summary.bullet_points:
|
|
47
|
+
rc.log(f"• {point}")
|
|
48
|
+
return summary
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Tests for the summarize example."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from unittest.mock import MagicMock
|
|
6
|
+
|
|
7
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
8
|
+
|
|
9
|
+
from summarize import SummarizeConfig, Summary, run
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_returns_structured_summary():
|
|
13
|
+
config = SummarizeConfig(text="Functualize is a CLI framework", max_points=2)
|
|
14
|
+
summary = run(config, MagicMock())
|
|
15
|
+
assert isinstance(summary, Summary)
|
|
16
|
+
assert len(summary.bullet_points) == 2
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "functualize-ai"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "AI Domain SDK for functualize — LLM interaction capabilities"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "Mohammad Hakim Adiprasetya", email = "viltohmyst@gmail.com" }
|
|
9
|
+
]
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"pydantic>=2.0.0",
|
|
13
|
+
"functualize-state",
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.entry-points."functualize.domains"]
|
|
25
|
+
ai = "functualize_ai._metadata:domain_metadata"
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
dev = [
|
|
29
|
+
"pytest>=7.4.0",
|
|
30
|
+
"pytest-cov>=4.1.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[build-system]
|
|
34
|
+
requires = ["hatchling"]
|
|
35
|
+
build-backend = "hatchling.build"
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.wheel]
|
|
38
|
+
packages = ["src/functualize_ai"]
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""functualize-ai — AI Domain SDK.
|
|
2
|
+
|
|
3
|
+
Provides the AI capability class, AIProvider protocol, ToolScope builder,
|
|
4
|
+
shared types, configuration, errors, event constants, gate strategy,
|
|
5
|
+
and testing doubles for LLM interaction capabilities.
|
|
6
|
+
|
|
7
|
+
Uses lazy imports via __getattr__ to avoid loading heavy dependencies
|
|
8
|
+
(pydantic, importlib.metadata) until first attribute access. This keeps
|
|
9
|
+
domain metadata discovery fast (~5ms instead of ~360ms).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
# Only domain_metadata is loaded eagerly (needed for entry point discovery)
|
|
15
|
+
from functualize_ai._metadata import domain_metadata
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
# Capability Class
|
|
19
|
+
"AI",
|
|
20
|
+
# Protocol
|
|
21
|
+
"AIProvider",
|
|
22
|
+
# ToolScope
|
|
23
|
+
"ToolScope",
|
|
24
|
+
# Types
|
|
25
|
+
"AIResult",
|
|
26
|
+
"TokenUsage",
|
|
27
|
+
"ToolDef",
|
|
28
|
+
"AILimits",
|
|
29
|
+
"ToolCallRecord",
|
|
30
|
+
# Config
|
|
31
|
+
"AIConfig",
|
|
32
|
+
# Errors
|
|
33
|
+
"AINotAvailableError",
|
|
34
|
+
"BudgetExceededError",
|
|
35
|
+
"ToolNotPermittedError",
|
|
36
|
+
# Event Constants
|
|
37
|
+
"AI_CALL_STARTED",
|
|
38
|
+
"AI_CALL_COMPLETED",
|
|
39
|
+
"AI_CALL_FAILED",
|
|
40
|
+
"AI_BUDGET_EXCEEDED",
|
|
41
|
+
"AI_TOOL_CALLED",
|
|
42
|
+
# Gate Strategy
|
|
43
|
+
"AI_INBOUND_STRATEGY_NAME",
|
|
44
|
+
"AI_INBOUND_PRESET_NAME",
|
|
45
|
+
"AI_INBOUND_PRESET_STRATEGIES",
|
|
46
|
+
"AI_PRESET_NAME",
|
|
47
|
+
"AI_PRESET_STRATEGIES",
|
|
48
|
+
"AIInboundGateResolver",
|
|
49
|
+
"register_ai_inbound_gate_strategy",
|
|
50
|
+
# State Fallback
|
|
51
|
+
"EphemeralStateBackend",
|
|
52
|
+
"StrictStateBackendWrapper",
|
|
53
|
+
"resolve_ai_state_backend",
|
|
54
|
+
# Provider Discovery
|
|
55
|
+
"discover_ai_providers",
|
|
56
|
+
"select_ai_provider",
|
|
57
|
+
"resolve_ai_provider",
|
|
58
|
+
# Metadata
|
|
59
|
+
"domain_metadata",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
# Lazy import mapping: attribute name → (module, name)
|
|
63
|
+
_LAZY_IMPORTS: dict[str, tuple[str, str]] = {
|
|
64
|
+
# _ai
|
|
65
|
+
"AI": ("functualize_ai._ai", "AI"),
|
|
66
|
+
# _config
|
|
67
|
+
"AIConfig": ("functualize_ai._config", "AIConfig"),
|
|
68
|
+
# _errors
|
|
69
|
+
"AINotAvailableError": ("functualize_ai._errors", "AINotAvailableError"),
|
|
70
|
+
"BudgetExceededError": ("functualize_ai._errors", "BudgetExceededError"),
|
|
71
|
+
"ToolNotPermittedError": ("functualize_ai._errors", "ToolNotPermittedError"),
|
|
72
|
+
# _events
|
|
73
|
+
"AI_BUDGET_EXCEEDED": ("functualize_ai._events", "AI_BUDGET_EXCEEDED"),
|
|
74
|
+
"AI_CALL_COMPLETED": ("functualize_ai._events", "AI_CALL_COMPLETED"),
|
|
75
|
+
"AI_CALL_FAILED": ("functualize_ai._events", "AI_CALL_FAILED"),
|
|
76
|
+
"AI_CALL_STARTED": ("functualize_ai._events", "AI_CALL_STARTED"),
|
|
77
|
+
"AI_TOOL_CALLED": ("functualize_ai._events", "AI_TOOL_CALLED"),
|
|
78
|
+
# _gate_strategy
|
|
79
|
+
"AI_INBOUND_PRESET_NAME": (
|
|
80
|
+
"functualize_ai._gate_strategy",
|
|
81
|
+
"AI_INBOUND_PRESET_NAME",
|
|
82
|
+
),
|
|
83
|
+
"AI_INBOUND_PRESET_STRATEGIES": (
|
|
84
|
+
"functualize_ai._gate_strategy",
|
|
85
|
+
"AI_INBOUND_PRESET_STRATEGIES",
|
|
86
|
+
),
|
|
87
|
+
"AI_INBOUND_STRATEGY_NAME": (
|
|
88
|
+
"functualize_ai._gate_strategy",
|
|
89
|
+
"AI_INBOUND_STRATEGY_NAME",
|
|
90
|
+
),
|
|
91
|
+
"AI_PRESET_NAME": ("functualize_ai._gate_strategy", "AI_PRESET_NAME"),
|
|
92
|
+
"AI_PRESET_STRATEGIES": ("functualize_ai._gate_strategy", "AI_PRESET_STRATEGIES"),
|
|
93
|
+
"AIInboundGateResolver": (
|
|
94
|
+
"functualize_ai._gate_strategy",
|
|
95
|
+
"AIInboundGateResolver",
|
|
96
|
+
),
|
|
97
|
+
"register_ai_inbound_gate_strategy": (
|
|
98
|
+
"functualize_ai._gate_strategy",
|
|
99
|
+
"register_ai_inbound_gate_strategy",
|
|
100
|
+
),
|
|
101
|
+
# _protocols
|
|
102
|
+
"AIProvider": ("functualize_ai._protocols", "AIProvider"),
|
|
103
|
+
# _provider_discovery
|
|
104
|
+
"discover_ai_providers": (
|
|
105
|
+
"functualize_ai._provider_discovery",
|
|
106
|
+
"discover_ai_providers",
|
|
107
|
+
),
|
|
108
|
+
"resolve_ai_provider": (
|
|
109
|
+
"functualize_ai._provider_discovery",
|
|
110
|
+
"resolve_ai_provider",
|
|
111
|
+
),
|
|
112
|
+
"select_ai_provider": (
|
|
113
|
+
"functualize_ai._provider_discovery",
|
|
114
|
+
"select_ai_provider",
|
|
115
|
+
),
|
|
116
|
+
# _state_fallback
|
|
117
|
+
"EphemeralStateBackend": (
|
|
118
|
+
"functualize_ai._state_fallback",
|
|
119
|
+
"EphemeralStateBackend",
|
|
120
|
+
),
|
|
121
|
+
"StrictStateBackendWrapper": (
|
|
122
|
+
"functualize_ai._state_fallback",
|
|
123
|
+
"StrictStateBackendWrapper",
|
|
124
|
+
),
|
|
125
|
+
"resolve_ai_state_backend": (
|
|
126
|
+
"functualize_ai._state_fallback",
|
|
127
|
+
"resolve_ai_state_backend",
|
|
128
|
+
),
|
|
129
|
+
# _tool_scope
|
|
130
|
+
"ToolScope": ("functualize_ai._tool_scope", "ToolScope"),
|
|
131
|
+
# _types
|
|
132
|
+
"AILimits": ("functualize_ai._types", "AILimits"),
|
|
133
|
+
"AIResult": ("functualize_ai._types", "AIResult"),
|
|
134
|
+
"TokenUsage": ("functualize_ai._types", "TokenUsage"),
|
|
135
|
+
"ToolCallRecord": ("functualize_ai._types", "ToolCallRecord"),
|
|
136
|
+
"ToolDef": ("functualize_ai._types", "ToolDef"),
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def __getattr__(name: str) -> object:
|
|
141
|
+
if name in _LAZY_IMPORTS:
|
|
142
|
+
module_path, attr_name = _LAZY_IMPORTS[name]
|
|
143
|
+
import importlib
|
|
144
|
+
|
|
145
|
+
module = importlib.import_module(module_path)
|
|
146
|
+
value = getattr(module, attr_name)
|
|
147
|
+
# Cache in module globals to avoid repeated __getattr__ calls
|
|
148
|
+
globals()[name] = value
|
|
149
|
+
return value
|
|
150
|
+
raise AttributeError(f"module 'functualize_ai' has no attribute {name!r}")
|