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.
Files changed (27) hide show
  1. functualize_ai-0.1.0/.gitignore +101 -0
  2. functualize_ai-0.1.0/PKG-INFO +134 -0
  3. functualize_ai-0.1.0/README.md +114 -0
  4. functualize_ai-0.1.0/examples/README.md +13 -0
  5. functualize_ai-0.1.0/examples/summarize/summarize.py +48 -0
  6. functualize_ai-0.1.0/examples/summarize/test_summarize.py +16 -0
  7. functualize_ai-0.1.0/pyproject.toml +38 -0
  8. functualize_ai-0.1.0/src/functualize_ai/__init__.py +150 -0
  9. functualize_ai-0.1.0/src/functualize_ai/_ai.py +436 -0
  10. functualize_ai-0.1.0/src/functualize_ai/_budget.py +150 -0
  11. functualize_ai-0.1.0/src/functualize_ai/_config.py +15 -0
  12. functualize_ai-0.1.0/src/functualize_ai/_errors.py +19 -0
  13. functualize_ai-0.1.0/src/functualize_ai/_events.py +7 -0
  14. functualize_ai-0.1.0/src/functualize_ai/_gate_strategy.py +141 -0
  15. functualize_ai-0.1.0/src/functualize_ai/_metadata.py +37 -0
  16. functualize_ai-0.1.0/src/functualize_ai/_protocols.py +83 -0
  17. functualize_ai-0.1.0/src/functualize_ai/_provider_discovery.py +207 -0
  18. functualize_ai-0.1.0/src/functualize_ai/_state_fallback.py +136 -0
  19. functualize_ai-0.1.0/src/functualize_ai/_tool_scope.py +464 -0
  20. functualize_ai-0.1.0/src/functualize_ai/_types.py +59 -0
  21. functualize_ai-0.1.0/src/functualize_ai/py.typed +0 -0
  22. functualize_ai-0.1.0/src/functualize_ai/testing/__init__.py +13 -0
  23. functualize_ai-0.1.0/src/functualize_ai/testing/_mock_ai.py +223 -0
  24. functualize_ai-0.1.0/tests/__init__.py +0 -0
  25. functualize_ai-0.1.0/tests/conftest.py +1 -0
  26. functualize_ai-0.1.0/tests/test_ai_capability.py +263 -0
  27. 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}")