llm-function 0.0.1__py3-none-any.whl

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 (157) hide show
  1. llm_function/.paa.tracking/.paa.config +12 -0
  2. llm_function/.paa.tracking/.paa.version +1 -0
  3. llm_function/.paa.tracking/git/dependencies/llm_func_deps/llm_function_config.py +35 -0
  4. llm_function/.paa.tracking/git/dependencies/llm_func_deps/tool_registry.py +250 -0
  5. llm_function/.paa.tracking/git/docs_cache/llm_function.md +0 -0
  6. llm_function/.paa.tracking/git/module/llm_function.py +250 -0
  7. llm_function/.paa.tracking/git/tracking/release_notes.md +5 -0
  8. llm_function/.paa.tracking/git_repo/COMMIT_EDITMSG +1 -0
  9. llm_function/.paa.tracking/git_repo/HEAD +1 -0
  10. llm_function/.paa.tracking/git_repo/config +8 -0
  11. llm_function/.paa.tracking/git_repo/description +1 -0
  12. llm_function/.paa.tracking/git_repo/hooks/applypatch-msg.sample +15 -0
  13. llm_function/.paa.tracking/git_repo/hooks/commit-msg.sample +24 -0
  14. llm_function/.paa.tracking/git_repo/hooks/fsmonitor-watchman.sample +174 -0
  15. llm_function/.paa.tracking/git_repo/hooks/post-update.sample +8 -0
  16. llm_function/.paa.tracking/git_repo/hooks/pre-applypatch.sample +14 -0
  17. llm_function/.paa.tracking/git_repo/hooks/pre-commit.sample +49 -0
  18. llm_function/.paa.tracking/git_repo/hooks/pre-merge-commit.sample +13 -0
  19. llm_function/.paa.tracking/git_repo/hooks/pre-push.sample +53 -0
  20. llm_function/.paa.tracking/git_repo/hooks/pre-rebase.sample +169 -0
  21. llm_function/.paa.tracking/git_repo/hooks/pre-receive.sample +24 -0
  22. llm_function/.paa.tracking/git_repo/hooks/prepare-commit-msg.sample +42 -0
  23. llm_function/.paa.tracking/git_repo/hooks/push-to-checkout.sample +78 -0
  24. llm_function/.paa.tracking/git_repo/hooks/sendemail-validate.sample +77 -0
  25. llm_function/.paa.tracking/git_repo/hooks/update.sample +128 -0
  26. llm_function/.paa.tracking/git_repo/index +0 -0
  27. llm_function/.paa.tracking/git_repo/info/exclude +6 -0
  28. llm_function/.paa.tracking/git_repo/logs/HEAD +1 -0
  29. llm_function/.paa.tracking/git_repo/logs/refs/heads/master +1 -0
  30. llm_function/.paa.tracking/git_repo/objects/13/9201e8ffeb4e42262e90eeaefd5267debb6e1d +1 -0
  31. llm_function/.paa.tracking/git_repo/objects/23/d10b479e3102724a6eb510225cc24bb2ce9d78 +0 -0
  32. llm_function/.paa.tracking/git_repo/objects/2d/fa3abff2848ff521ead3abb4364bd2c3ca01a6 +0 -0
  33. llm_function/.paa.tracking/git_repo/objects/45/6fc88abc846d76e0f82a3ef180425e497f8281 +0 -0
  34. llm_function/.paa.tracking/git_repo/objects/55/d196aad68e2cc20a2ea496529d9dabeb75bb0e +0 -0
  35. llm_function/.paa.tracking/git_repo/objects/63/9282ebc80b9db9b677374a7c16bc7472d5e71c +0 -0
  36. llm_function/.paa.tracking/git_repo/objects/91/f1d4c0830abe4ebb0902dc052f5dfe9e8fade7 +0 -0
  37. llm_function/.paa.tracking/git_repo/objects/9d/2a8151dd42e6abfffbbbd674b850a7066daa1a +0 -0
  38. llm_function/.paa.tracking/git_repo/objects/aa/0b3847c3f0683eed107a405b9443ec040c3eec +0 -0
  39. llm_function/.paa.tracking/git_repo/objects/cf/024b71ae427969a985d86a267adf154f526cd8 +0 -0
  40. llm_function/.paa.tracking/git_repo/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 +0 -0
  41. llm_function/.paa.tracking/git_repo/objects/fb/834f2f59b432c0688699b91926899c0e0f2a5d +0 -0
  42. llm_function/.paa.tracking/git_repo/refs/heads/master +1 -0
  43. llm_function/.paa.tracking/git_repo/refs/tags/v0.0.1 +1 -0
  44. llm_function/.paa.tracking/lsts_package_versions.yml +3 -0
  45. llm_function/.paa.tracking/package_licenses.json +1 -0
  46. llm_function/.paa.tracking/package_mapping.json +1 -0
  47. llm_function/.paa.tracking/python_modules/components/llm_func_deps/llm_function_config.py +35 -0
  48. llm_function/.paa.tracking/python_modules/components/llm_func_deps/tool_registry.py +250 -0
  49. llm_function/.paa.tracking/python_modules/llm_function.py +250 -0
  50. llm_function/.paa.tracking/release_notes.md +5 -0
  51. llm_function/.paa.tracking/version_logs.csv +5 -0
  52. llm_function/__init__.py +17 -0
  53. llm_function/artifacts/.paa.tracking/git/dependencies/llm_func_deps/llm_function_config.py +35 -0
  54. llm_function/artifacts/.paa.tracking/git/dependencies/llm_func_deps/tool_registry.py +250 -0
  55. llm_function/artifacts/.paa.tracking/git/docs_cache/llm_function.md +0 -0
  56. llm_function/artifacts/.paa.tracking/git/module/llm_function.py +250 -0
  57. llm_function/artifacts/.paa.tracking/git/tracking/release_notes.md +5 -0
  58. llm_function/artifacts/.paa.tracking/git_repo/COMMIT_EDITMSG +1 -0
  59. llm_function/artifacts/.paa.tracking/git_repo/HEAD +1 -0
  60. llm_function/artifacts/.paa.tracking/git_repo/config +8 -0
  61. llm_function/artifacts/.paa.tracking/git_repo/description +1 -0
  62. llm_function/artifacts/.paa.tracking/git_repo/hooks/applypatch-msg.sample +15 -0
  63. llm_function/artifacts/.paa.tracking/git_repo/hooks/commit-msg.sample +24 -0
  64. llm_function/artifacts/.paa.tracking/git_repo/hooks/fsmonitor-watchman.sample +174 -0
  65. llm_function/artifacts/.paa.tracking/git_repo/hooks/post-update.sample +8 -0
  66. llm_function/artifacts/.paa.tracking/git_repo/hooks/pre-applypatch.sample +14 -0
  67. llm_function/artifacts/.paa.tracking/git_repo/hooks/pre-commit.sample +49 -0
  68. llm_function/artifacts/.paa.tracking/git_repo/hooks/pre-merge-commit.sample +13 -0
  69. llm_function/artifacts/.paa.tracking/git_repo/hooks/pre-push.sample +53 -0
  70. llm_function/artifacts/.paa.tracking/git_repo/hooks/pre-rebase.sample +169 -0
  71. llm_function/artifacts/.paa.tracking/git_repo/hooks/pre-receive.sample +24 -0
  72. llm_function/artifacts/.paa.tracking/git_repo/hooks/prepare-commit-msg.sample +42 -0
  73. llm_function/artifacts/.paa.tracking/git_repo/hooks/push-to-checkout.sample +78 -0
  74. llm_function/artifacts/.paa.tracking/git_repo/hooks/sendemail-validate.sample +77 -0
  75. llm_function/artifacts/.paa.tracking/git_repo/hooks/update.sample +128 -0
  76. llm_function/artifacts/.paa.tracking/git_repo/index +0 -0
  77. llm_function/artifacts/.paa.tracking/git_repo/info/exclude +6 -0
  78. llm_function/artifacts/.paa.tracking/git_repo/logs/HEAD +1 -0
  79. llm_function/artifacts/.paa.tracking/git_repo/logs/refs/heads/master +1 -0
  80. llm_function/artifacts/.paa.tracking/git_repo/objects/13/9201e8ffeb4e42262e90eeaefd5267debb6e1d +1 -0
  81. llm_function/artifacts/.paa.tracking/git_repo/objects/23/d10b479e3102724a6eb510225cc24bb2ce9d78 +0 -0
  82. llm_function/artifacts/.paa.tracking/git_repo/objects/2d/fa3abff2848ff521ead3abb4364bd2c3ca01a6 +0 -0
  83. llm_function/artifacts/.paa.tracking/git_repo/objects/45/6fc88abc846d76e0f82a3ef180425e497f8281 +0 -0
  84. llm_function/artifacts/.paa.tracking/git_repo/objects/55/d196aad68e2cc20a2ea496529d9dabeb75bb0e +0 -0
  85. llm_function/artifacts/.paa.tracking/git_repo/objects/63/9282ebc80b9db9b677374a7c16bc7472d5e71c +0 -0
  86. llm_function/artifacts/.paa.tracking/git_repo/objects/91/f1d4c0830abe4ebb0902dc052f5dfe9e8fade7 +0 -0
  87. llm_function/artifacts/.paa.tracking/git_repo/objects/9d/2a8151dd42e6abfffbbbd674b850a7066daa1a +0 -0
  88. llm_function/artifacts/.paa.tracking/git_repo/objects/aa/0b3847c3f0683eed107a405b9443ec040c3eec +0 -0
  89. llm_function/artifacts/.paa.tracking/git_repo/objects/cf/024b71ae427969a985d86a267adf154f526cd8 +0 -0
  90. llm_function/artifacts/.paa.tracking/git_repo/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 +0 -0
  91. llm_function/artifacts/.paa.tracking/git_repo/objects/fb/834f2f59b432c0688699b91926899c0e0f2a5d +0 -0
  92. llm_function/artifacts/.paa.tracking/git_repo/refs/heads/master +1 -0
  93. llm_function/artifacts/.paa.tracking/git_repo/refs/tags/v0.0.1 +1 -0
  94. llm_function/llm_function.py +518 -0
  95. llm_function/mkdocs/docs/css/extra.css +38 -0
  96. llm_function/mkdocs/docs/index.md +24 -0
  97. llm_function/mkdocs/docs/llm_function_tools.md +168 -0
  98. llm_function/mkdocs/docs/release-notes.md +5 -0
  99. llm_function/mkdocs/mkdocs.yml +37 -0
  100. llm_function/mkdocs/site/404.html +370 -0
  101. llm_function/mkdocs/site/assets/images/favicon.png +0 -0
  102. llm_function/mkdocs/site/assets/javascripts/bundle.fe8b6f2b.min.js +29 -0
  103. llm_function/mkdocs/site/assets/javascripts/bundle.fe8b6f2b.min.js.map +7 -0
  104. llm_function/mkdocs/site/assets/javascripts/lunr/min/lunr.ar.min.js +1 -0
  105. llm_function/mkdocs/site/assets/javascripts/lunr/min/lunr.da.min.js +18 -0
  106. llm_function/mkdocs/site/assets/javascripts/lunr/min/lunr.de.min.js +18 -0
  107. llm_function/mkdocs/site/assets/javascripts/lunr/min/lunr.du.min.js +18 -0
  108. llm_function/mkdocs/site/assets/javascripts/lunr/min/lunr.el.min.js +1 -0
  109. llm_function/mkdocs/site/assets/javascripts/lunr/min/lunr.es.min.js +18 -0
  110. llm_function/mkdocs/site/assets/javascripts/lunr/min/lunr.fi.min.js +18 -0
  111. llm_function/mkdocs/site/assets/javascripts/lunr/min/lunr.fr.min.js +18 -0
  112. llm_function/mkdocs/site/assets/javascripts/lunr/min/lunr.he.min.js +1 -0
  113. llm_function/mkdocs/site/assets/javascripts/lunr/min/lunr.hi.min.js +1 -0
  114. llm_function/mkdocs/site/assets/javascripts/lunr/min/lunr.hu.min.js +18 -0
  115. llm_function/mkdocs/site/assets/javascripts/lunr/min/lunr.hy.min.js +1 -0
  116. llm_function/mkdocs/site/assets/javascripts/lunr/min/lunr.it.min.js +18 -0
  117. llm_function/mkdocs/site/assets/javascripts/lunr/min/lunr.ja.min.js +1 -0
  118. llm_function/mkdocs/site/assets/javascripts/lunr/min/lunr.jp.min.js +1 -0
  119. llm_function/mkdocs/site/assets/javascripts/lunr/min/lunr.kn.min.js +1 -0
  120. llm_function/mkdocs/site/assets/javascripts/lunr/min/lunr.ko.min.js +1 -0
  121. llm_function/mkdocs/site/assets/javascripts/lunr/min/lunr.multi.min.js +1 -0
  122. llm_function/mkdocs/site/assets/javascripts/lunr/min/lunr.nl.min.js +18 -0
  123. llm_function/mkdocs/site/assets/javascripts/lunr/min/lunr.no.min.js +18 -0
  124. llm_function/mkdocs/site/assets/javascripts/lunr/min/lunr.pt.min.js +18 -0
  125. llm_function/mkdocs/site/assets/javascripts/lunr/min/lunr.ro.min.js +18 -0
  126. llm_function/mkdocs/site/assets/javascripts/lunr/min/lunr.ru.min.js +18 -0
  127. llm_function/mkdocs/site/assets/javascripts/lunr/min/lunr.sa.min.js +1 -0
  128. llm_function/mkdocs/site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +1 -0
  129. llm_function/mkdocs/site/assets/javascripts/lunr/min/lunr.sv.min.js +18 -0
  130. llm_function/mkdocs/site/assets/javascripts/lunr/min/lunr.ta.min.js +1 -0
  131. llm_function/mkdocs/site/assets/javascripts/lunr/min/lunr.te.min.js +1 -0
  132. llm_function/mkdocs/site/assets/javascripts/lunr/min/lunr.th.min.js +1 -0
  133. llm_function/mkdocs/site/assets/javascripts/lunr/min/lunr.tr.min.js +18 -0
  134. llm_function/mkdocs/site/assets/javascripts/lunr/min/lunr.vi.min.js +1 -0
  135. llm_function/mkdocs/site/assets/javascripts/lunr/min/lunr.zh.min.js +1 -0
  136. llm_function/mkdocs/site/assets/javascripts/lunr/tinyseg.js +206 -0
  137. llm_function/mkdocs/site/assets/javascripts/lunr/wordcut.js +6708 -0
  138. llm_function/mkdocs/site/assets/javascripts/workers/search.b8dbb3d2.min.js +42 -0
  139. llm_function/mkdocs/site/assets/javascripts/workers/search.b8dbb3d2.min.js.map +7 -0
  140. llm_function/mkdocs/site/assets/stylesheets/main.3cba04c6.min.css +1 -0
  141. llm_function/mkdocs/site/assets/stylesheets/main.3cba04c6.min.css.map +1 -0
  142. llm_function/mkdocs/site/assets/stylesheets/palette.06af60db.min.css +1 -0
  143. llm_function/mkdocs/site/assets/stylesheets/palette.06af60db.min.css.map +1 -0
  144. llm_function/mkdocs/site/css/extra.css +38 -0
  145. llm_function/mkdocs/site/index.html +476 -0
  146. llm_function/mkdocs/site/llm_function_tools/index.html +622 -0
  147. llm_function/mkdocs/site/release-notes/index.html +468 -0
  148. llm_function/mkdocs/site/search/search_index.json +1 -0
  149. llm_function/mkdocs/site/sitemap.xml +3 -0
  150. llm_function/mkdocs/site/sitemap.xml.gz +0 -0
  151. llm_function/setup.py +47 -0
  152. llm_function-0.0.1.dist-info/METADATA +44 -0
  153. llm_function-0.0.1.dist-info/RECORD +157 -0
  154. llm_function-0.0.1.dist-info/WHEEL +5 -0
  155. llm_function-0.0.1.dist-info/licenses/LICENSE +201 -0
  156. llm_function-0.0.1.dist-info/licenses/NOTICE +4 -0
  157. llm_function-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,518 @@
1
+ """
2
+ `llm_function` helps you build reusable LLM functions with normal Python signatures.
3
+
4
+ You define a function with Pydantic input and output models, describe what it should do
5
+ in the docstring, and provide a set of available tools. At runtime, `llm_function`
6
+ uses [`workflow_auto_assembler`](https://pypi.org/project/workflow-auto-assembler/) to
7
+ assemble and execute a workflow that satisfies that typed function contract.
8
+
9
+ The result is an LLM-backed function that can be reused like any other Python function,
10
+ while still being grounded in explicit tools, schemas, and config.
11
+
12
+ Tool definition and discovery live in
13
+ [`llm_function_tools`](https://pypi.org/project/llm-function-tools/).
14
+ """
15
+
16
+ import asyncio
17
+ import attrs
18
+ import attrsx
19
+ import inspect
20
+ import threading
21
+ from functools import wraps
22
+ from typing import Any, Callable, Dict, List, Optional, Type, get_type_hints
23
+ from pydantic import BaseModel
24
+ from workflow_auto_assembler import WorkflowAutoAssembler
25
+ from typing import Any, Optional, Sequence
26
+ from pydantic import BaseModel, Field
27
+ from pathlib import Path
28
+ from typing import Any, Callable, Dict, List, Optional, Sequence, Union
29
+ from pydantic import BaseModel, Field, SkipValidation
30
+ from workflow_auto_assembler import LlmFunctionItem, make_uid
31
+ from llm_function_tools import (
32
+ ToolSpec,
33
+ load_tools_from_module,
34
+ load_tools_from_python_file,
35
+ tool_from_callable,
36
+ )
37
+
38
+ __design_choices__ = {}
39
+
40
+ class LlmRuntimeConfig(BaseModel):
41
+ """
42
+ Reusable runtime settings for llm_function execution.
43
+ """
44
+
45
+ llm_handler_params: dict = Field(description="Parameters passed to WorkflowAutoAssembler LLM handler.")
46
+ storage_path: Optional[str] = Field(default=None, description="Optional storage path for workflow persistence.")
47
+ force_replan: bool = Field(default=False, description="Force workflow replanning instead of reusing cached workflows.")
48
+ max_retry: Optional[int] = Field(default=None, description="Optional max retry override for planning loops.")
49
+ reset_loops: Optional[int] = Field(default=None, description="Optional reset loop override for planning loops.")
50
+ compare_params: Optional[dict] = Field(default=None, description="Optional compare parameters for workflow validation.")
51
+ test_params: Optional[list] = Field(default=None, description="Optional test cases for planning/validation.")
52
+
53
+
54
+ class LlmFunctionConfig(BaseModel):
55
+ """
56
+ Bundled llm_function configuration for future config-driven decorator usage.
57
+ """
58
+
59
+ runtime: LlmRuntimeConfig = Field(description="Runtime settings for workflow execution.")
60
+ tool_sources: Optional[Sequence[Any]] = Field(default=None, description="Configured tool sources for runtime loading.")
61
+ tool_registry: Optional[Any] = Field(default=None, description="Optional prebuilt tool registry.")
62
+
63
+ model_config = {
64
+ "arbitrary_types_allowed": True,
65
+ }
66
+
67
+ class ResolvedTool(BaseModel):
68
+ """
69
+ Runtime-ready tool definition with provenance metadata.
70
+ """
71
+
72
+ tool_spec: ToolSpec = Field(description="Normalized tool definition.")
73
+ func: SkipValidation[Callable[..., Any]] = Field(description="Runtime callable for the tool.")
74
+ source_type: str = Field(description="Source kind, for example file or module.")
75
+ location_type: str = Field(description="Location kind, for example local or external.")
76
+ package_name: Optional[str] = Field(default=None, description="Optional package name.")
77
+ package_version: Optional[str] = Field(default=None, description="Optional resolved package version.")
78
+ module_name: Optional[str] = Field(default=None, description="Python module that defined the tool.")
79
+ file_path: Optional[str] = Field(default=None, description="Local file path when the tool was loaded from a file.")
80
+ origin_ref: Optional[str] = Field(default=None, description="Original source reference used to load the tool.")
81
+ metadata: dict = Field(default_factory=dict, description="Additional runtime metadata.")
82
+
83
+ model_config = {
84
+ "arbitrary_types_allowed": True,
85
+ }
86
+
87
+
88
+ @attrsx.define
89
+ class InMemoryToolSource:
90
+ """
91
+ Resolve tools directly from callables, ToolSpec objects, or ResolvedTool objects.
92
+ """
93
+
94
+ tools: Sequence[Union[Callable[..., Any], ToolSpec, ResolvedTool]] = attrs.field()
95
+ location_type: str = attrs.field(default="local")
96
+ package_name: Optional[str] = attrs.field(default=None)
97
+ package_version: Optional[str] = attrs.field(default=None)
98
+ origin_ref: Optional[str] = attrs.field(default=None)
99
+
100
+ def __attrs_post_init__(self):
101
+ self.tools = list(self.tools)
102
+
103
+ def load_tools(self) -> List[ResolvedTool]:
104
+ """
105
+ Resolve in-memory tools into runtime tool records.
106
+ """
107
+
108
+ resolved_tools: List[ResolvedTool] = []
109
+ for item in self.tools:
110
+ if isinstance(item, ResolvedTool):
111
+ resolved_tools.append(item)
112
+ continue
113
+
114
+ tool_spec = item if isinstance(item, ToolSpec) else tool_from_callable(item)
115
+ resolved_tools.append(
116
+ ResolvedTool(
117
+ tool_spec=tool_spec,
118
+ func=tool_spec.func,
119
+ source_type="memory",
120
+ location_type=self.location_type,
121
+ package_name=self.package_name,
122
+ package_version=self.package_version,
123
+ module_name=getattr(tool_spec.func, "__module__", None),
124
+ origin_ref=self.origin_ref,
125
+ )
126
+ )
127
+
128
+ return resolved_tools
129
+
130
+
131
+ @attrsx.define
132
+ class PythonModuleToolSource:
133
+ """
134
+ Resolve tools from an importable Python module.
135
+ """
136
+
137
+ module_name: str = attrs.field()
138
+ include_plain_typed: bool = attrs.field(default=False)
139
+ location_type: str = attrs.field(default="local")
140
+ package_name: Optional[str] = attrs.field(default=None)
141
+ package_version: Optional[str] = attrs.field(default=None)
142
+
143
+ def load_tools(self) -> List[ResolvedTool]:
144
+ """
145
+ Import module and resolve tools declared inside it.
146
+ """
147
+
148
+ resolved_tools: List[ResolvedTool] = []
149
+ for tool_spec in load_tools_from_module(
150
+ self.module_name,
151
+ include_plain_typed=self.include_plain_typed,
152
+ ):
153
+ resolved_tools.append(
154
+ ResolvedTool(
155
+ tool_spec=tool_spec,
156
+ func=tool_spec.func,
157
+ source_type="module",
158
+ location_type=self.location_type,
159
+ package_name=self.package_name,
160
+ package_version=self.package_version,
161
+ module_name=self.module_name,
162
+ origin_ref=self.module_name,
163
+ )
164
+ )
165
+
166
+ return resolved_tools
167
+
168
+
169
+ @attrsx.define
170
+ class PythonFileToolSource:
171
+ """
172
+ Resolve tools from a standalone Python file path.
173
+ """
174
+
175
+ file_path: str = attrs.field()
176
+ include_plain_typed: bool = attrs.field(default=False)
177
+ location_type: str = attrs.field(default="local")
178
+ package_name: Optional[str] = attrs.field(default=None)
179
+ package_version: Optional[str] = attrs.field(default=None)
180
+ module_name: Optional[str] = attrs.field(default=None)
181
+
182
+ def load_tools(self) -> List[ResolvedTool]:
183
+ """
184
+ Import file and resolve tools declared inside it.
185
+ """
186
+
187
+ resolved_path = str(Path(self.file_path).expanduser().resolve())
188
+ resolved_tools: List[ResolvedTool] = []
189
+ for tool_spec in load_tools_from_python_file(
190
+ resolved_path,
191
+ include_plain_typed=self.include_plain_typed,
192
+ module_name=self.module_name,
193
+ ):
194
+ resolved_tools.append(
195
+ ResolvedTool(
196
+ tool_spec=tool_spec,
197
+ func=tool_spec.func,
198
+ source_type="file",
199
+ location_type=self.location_type,
200
+ package_name=self.package_name,
201
+ package_version=self.package_version,
202
+ module_name=getattr(tool_spec.func, "__module__", self.module_name),
203
+ file_path=resolved_path,
204
+ origin_ref=resolved_path,
205
+ )
206
+ )
207
+
208
+ return resolved_tools
209
+
210
+
211
+ @attrsx.define
212
+ class ToolRegistry:
213
+ """
214
+ Aggregate tools from one or more sources and expose WAA-compatible tool lists.
215
+ """
216
+
217
+ sources: Sequence[object] = attrs.field()
218
+ _resolved_tools: Optional[List[ResolvedTool]] = attrs.field(default=None)
219
+
220
+ def __attrs_post_init__(self):
221
+ self.sources = list(self.sources)
222
+
223
+ def load_tools(self, reload: bool = False) -> List[ResolvedTool]:
224
+ """
225
+ Resolve all configured sources into runtime tools.
226
+ """
227
+
228
+ if self._resolved_tools is not None and not reload:
229
+ return list(self._resolved_tools)
230
+
231
+ resolved_tools: List[ResolvedTool] = []
232
+ seen_keys = set()
233
+
234
+ for source in self.sources:
235
+ for resolved_tool in source.load_tools():
236
+ dedupe_key = (
237
+ resolved_tool.tool_spec.name,
238
+ resolved_tool.module_name,
239
+ resolved_tool.file_path,
240
+ resolved_tool.package_name,
241
+ resolved_tool.package_version,
242
+ )
243
+ if dedupe_key in seen_keys:
244
+ continue
245
+ seen_keys.add(dedupe_key)
246
+ resolved_tools.append(resolved_tool)
247
+
248
+ self._resolved_tools = resolved_tools
249
+ return list(self._resolved_tools)
250
+
251
+ @staticmethod
252
+ def _safe_source_code(func: Callable[..., Any]) -> str:
253
+ """
254
+ Best-effort source capture for stable tool hashing.
255
+ """
256
+
257
+ try:
258
+ return inspect.getsource(func)
259
+ except (OSError, TypeError):
260
+ return getattr(func, "__qualname__", getattr(func, "__name__", ""))
261
+
262
+ def build_available_tools(self) -> Dict[str, Any]:
263
+ """
264
+ Convert resolved tools into WorkflowAutoAssembler-compatible structures.
265
+ """
266
+
267
+ available_functions: List[LlmFunctionItem] = []
268
+ available_callables: Dict[str, Callable[..., Any]] = {}
269
+
270
+ for resolved_tool in self.load_tools():
271
+ llm_func_item = {
272
+ "name": resolved_tool.tool_spec.name,
273
+ "description": resolved_tool.tool_spec.description,
274
+ "input_schema_json": resolved_tool.tool_spec.input_model.model_json_schema(),
275
+ "output_schema_json": resolved_tool.tool_spec.output_model.model_json_schema(),
276
+ "code": self._safe_source_code(resolved_tool.func),
277
+ }
278
+ func_id = make_uid(d=llm_func_item)
279
+
280
+ available_functions.append(
281
+ LlmFunctionItem(
282
+ func_id=func_id,
283
+ name=llm_func_item["name"],
284
+ description=llm_func_item["description"],
285
+ input_schema_json=llm_func_item["input_schema_json"],
286
+ output_schema_json=llm_func_item["output_schema_json"],
287
+ )
288
+ )
289
+ available_callables[func_id] = resolved_tool.func
290
+
291
+ return {
292
+ "available_functions": available_functions,
293
+ "available_callables": available_callables,
294
+ "resolved_tools": self.load_tools(),
295
+ }
296
+
297
+ __package_metadata__ = {
298
+ "author": "Kyrylo Mordan",
299
+ "author_email": "parachute.repo@gmail.com",
300
+ "description": "Llm function is a decorator that uses llm to assemble reusable workflows from available tools to match input and output models.",
301
+ "url" : 'https://kiril-mordan.github.io/adaptive-reusables/llm-function/',
302
+ }
303
+
304
+
305
+ @attrsx.define
306
+ class LlmFunction:
307
+ available_functions: Optional[List[Any]] = attrs.field(default=None)
308
+ available_callables: Optional[Dict[str, Callable[..., Any]]] = attrs.field(default=None)
309
+ tool_registry: Optional[ToolRegistry] = attrs.field(default=None)
310
+ tool_sources: Optional[List[object]] = attrs.field(default=None)
311
+ config: Optional[LlmFunctionConfig] = attrs.field(default=None)
312
+ llm_handler_params: Optional[dict] = attrs.field(default=None)
313
+ storage_path: Optional[str] = attrs.field(default=None)
314
+ force_replan: bool = attrs.field(default=False)
315
+ max_retry: Optional[int] = attrs.field(default=None)
316
+ reset_loops: Optional[int] = attrs.field(default=None)
317
+ compare_params: Optional[dict] = attrs.field(default=None)
318
+ test_params: Optional[list] = attrs.field(default=None)
319
+ resolved_tools: Optional[Dict[str, Any]] = attrs.field(default=None, init=False)
320
+
321
+ def __attrs_post_init__(self):
322
+ self._apply_config_defaults()
323
+
324
+ if self.llm_handler_params is None:
325
+ raise ValueError("llm_handler_params is required either directly or via config.")
326
+
327
+ self.resolved_tools = self._resolve_available_tools()
328
+
329
+ def _apply_config_defaults(self):
330
+ if self.config is None:
331
+ return
332
+
333
+ if self.llm_handler_params is None:
334
+ self.llm_handler_params = self.config.runtime.llm_handler_params
335
+ if self.storage_path is None:
336
+ self.storage_path = self.config.runtime.storage_path
337
+ if self.force_replan is False:
338
+ self.force_replan = self.config.runtime.force_replan
339
+ if self.max_retry is None:
340
+ self.max_retry = self.config.runtime.max_retry
341
+ if self.reset_loops is None:
342
+ self.reset_loops = self.config.runtime.reset_loops
343
+ if self.compare_params is None:
344
+ self.compare_params = self.config.runtime.compare_params
345
+ if self.test_params is None:
346
+ self.test_params = self.config.runtime.test_params
347
+ if self.tool_registry is None:
348
+ self.tool_registry = self.config.tool_registry
349
+ if self.tool_sources is None and self.config.tool_sources is not None:
350
+ self.tool_sources = list(self.config.tool_sources)
351
+
352
+ def _normalize_task_description(self, func: Callable[..., Any]) -> str:
353
+ task_description = inspect.cleandoc(func.__doc__ or "").strip()
354
+ if not task_description:
355
+ raise ValueError("Decorated function must define a docstring to use as task_description.")
356
+
357
+ return task_description
358
+
359
+ def _extract_io_models(self, func: Callable[..., Any]) -> tuple[Type[BaseModel], Type[BaseModel], str]:
360
+ signature = inspect.signature(func)
361
+ params = list(signature.parameters.values())
362
+ if len(params) != 1:
363
+ raise ValueError("Decorated function must accept exactly one parameter annotated with a BaseModel subclass.")
364
+
365
+ input_param = params[0]
366
+ hints = get_type_hints(func)
367
+ input_model = hints.get(input_param.name)
368
+ output_model = hints.get("return")
369
+
370
+ if not inspect.isclass(input_model) or not issubclass(input_model, BaseModel):
371
+ raise TypeError("Decorated function input annotation must be a Pydantic BaseModel subclass.")
372
+
373
+ if not inspect.isclass(output_model) or not issubclass(output_model, BaseModel):
374
+ raise TypeError("Decorated function return annotation must be a Pydantic BaseModel subclass.")
375
+
376
+ return input_model, output_model, input_param.name
377
+
378
+ def _coerce_run_inputs(self, input_model: Type[BaseModel], run_inputs: Any) -> BaseModel:
379
+ if isinstance(run_inputs, input_model):
380
+ return run_inputs
381
+
382
+ if hasattr(input_model, "model_validate"):
383
+ return input_model.model_validate(run_inputs)
384
+
385
+ return input_model.parse_obj(run_inputs)
386
+
387
+ def _run_coro_blocking(self, coro):
388
+ try:
389
+ asyncio.get_running_loop()
390
+ except RuntimeError:
391
+ return asyncio.run(coro)
392
+
393
+ result: Dict[str, Any] = {}
394
+ error: Dict[str, BaseException] = {}
395
+
396
+ def _thread_main():
397
+ try:
398
+ result["value"] = asyncio.run(coro)
399
+ except BaseException as exc: # pragma: no cover
400
+ error["value"] = exc
401
+
402
+ thread = threading.Thread(target=_thread_main, daemon=True)
403
+ thread.start()
404
+ thread.join()
405
+
406
+ if "value" in error:
407
+ raise error["value"]
408
+
409
+ return result["value"]
410
+
411
+ def _resolve_available_tools(self) -> Dict[str, Any]:
412
+ if self.tool_registry is not None and self.tool_sources is not None:
413
+ raise ValueError("Use either tool_registry or tool_sources, not both.")
414
+
415
+ if self.tool_registry is not None:
416
+ return self.tool_registry.build_available_tools()
417
+
418
+ if self.tool_sources is not None:
419
+ return ToolRegistry(sources=self.tool_sources).build_available_tools()
420
+
421
+ if self.available_functions is None or self.available_callables is None:
422
+ raise ValueError(
423
+ "Provide either available_functions and available_callables, or tool_registry/tool_sources."
424
+ )
425
+
426
+ return {
427
+ "available_functions": self.available_functions,
428
+ "available_callables": self.available_callables,
429
+ "resolved_tools": None,
430
+ }
431
+
432
+ def as_decorator(self):
433
+ def decorator(func: Callable[..., Any]):
434
+ signature = inspect.signature(func)
435
+ task_description = self._normalize_task_description(func)
436
+ input_model, output_model, input_name = self._extract_io_models(func)
437
+
438
+ async def _invoke_async(run_inputs: Any):
439
+ wa = WorkflowAutoAssembler(
440
+ available_functions=self.resolved_tools["available_functions"],
441
+ available_callables=self.resolved_tools["available_callables"],
442
+ storage_path=self.storage_path,
443
+ llm_handler_params=self.llm_handler_params,
444
+ )
445
+
446
+ return await wa.actualize_workflow(
447
+ task_description=task_description,
448
+ force_replan=self.force_replan,
449
+ run_inputs=self._coerce_run_inputs(input_model=input_model, run_inputs=run_inputs),
450
+ test_params=self.test_params,
451
+ compare_params=self.compare_params,
452
+ input_model=input_model,
453
+ output_model=output_model,
454
+ max_retry=self.max_retry,
455
+ reset_loops=self.reset_loops,
456
+ )
457
+
458
+ if inspect.iscoroutinefunction(func):
459
+
460
+ @wraps(func)
461
+ async def async_wrapper(*args, **kwargs):
462
+ bound = signature.bind(*args, **kwargs)
463
+ return await _invoke_async(bound.arguments[input_name])
464
+
465
+ async_wrapper.input_model = input_model
466
+ async_wrapper.output_model = output_model
467
+ async_wrapper.task_description = task_description
468
+ async_wrapper.ainvoke = _invoke_async
469
+ async_wrapper.resolved_tools = self.resolved_tools["resolved_tools"]
470
+ return async_wrapper
471
+
472
+ @wraps(func)
473
+ def sync_wrapper(*args, **kwargs):
474
+ bound = signature.bind(*args, **kwargs)
475
+ return self._run_coro_blocking(_invoke_async(bound.arguments[input_name]))
476
+
477
+ sync_wrapper.input_model = input_model
478
+ sync_wrapper.output_model = output_model
479
+ sync_wrapper.task_description = task_description
480
+ sync_wrapper.ainvoke = _invoke_async
481
+ sync_wrapper.resolved_tools = self.resolved_tools["resolved_tools"]
482
+ return sync_wrapper
483
+
484
+ return decorator
485
+
486
+
487
+ def llm_function(
488
+ *,
489
+ available_functions: Optional[List[Any]] = None,
490
+ available_callables: Optional[Dict[str, Callable[..., Any]]] = None,
491
+ tool_registry: Optional[ToolRegistry] = None,
492
+ tool_sources: Optional[List[object]] = None,
493
+ config: Optional[LlmFunctionConfig] = None,
494
+ llm_handler_params: Optional[dict] = None,
495
+ storage_path: Optional[str] = None,
496
+ force_replan: bool = False,
497
+ max_retry: Optional[int] = None,
498
+ reset_loops: Optional[int] = None,
499
+ compare_params: Optional[dict] = None,
500
+ test_params: Optional[list] = None,
501
+ ):
502
+ """
503
+ Decorate a typed function and route its calls through WorkflowAutoAssembler.
504
+ """
505
+ return LlmFunction(
506
+ available_functions=available_functions,
507
+ available_callables=available_callables,
508
+ tool_registry=tool_registry,
509
+ tool_sources=tool_sources,
510
+ config=config,
511
+ llm_handler_params=llm_handler_params,
512
+ storage_path=storage_path,
513
+ force_replan=force_replan,
514
+ max_retry=max_retry,
515
+ reset_loops=reset_loops,
516
+ compare_params=compare_params,
517
+ test_params=test_params,
518
+ ).as_decorator()
@@ -0,0 +1,38 @@
1
+ /* Ensure tables are scrollable horizontally */
2
+ table {
3
+ display: block;
4
+ width: 100%;
5
+ overflow-x: auto;
6
+ white-space: nowrap;
7
+ }
8
+
9
+ /* Ensure tables and their parent divs don't overflow the content area */
10
+ .dataframe {
11
+ display: block;
12
+ width: 100%;
13
+ overflow-x: auto;
14
+ white-space: nowrap;
15
+ }
16
+
17
+ .dataframe thead th {
18
+ text-align: right;
19
+ }
20
+
21
+ .dataframe tbody tr th {
22
+ vertical-align: top;
23
+ }
24
+
25
+ .dataframe tbody tr th:only-of-type {
26
+ vertical-align: middle;
27
+ }
28
+
29
+ /* Ensure the whole content area is scrollable */
30
+ .md-content__inner {
31
+ overflow-x: auto;
32
+ padding: 20px; /* Add some padding for better readability */
33
+ }
34
+
35
+ /* Fix layout issues caused by the theme */
36
+ .md-main__inner {
37
+ max-width: none;
38
+ }
@@ -0,0 +1,24 @@
1
+ # Intro
2
+
3
+ [![PyPiVersion](https://img.shields.io/pypi/v/llm-function)](https://pypi.org/project/llm-function/)
4
+
5
+ `llm_function` helps you build reusable LLM functions with normal Python signatures.
6
+
7
+ You define a function with Pydantic input and output models, describe what it should do
8
+ in the docstring, and provide a set of available tools. At runtime, `llm_function`
9
+ uses [`workflow_auto_assembler`](https://pypi.org/project/workflow-auto-assembler/) to
10
+ assemble and execute a workflow that satisfies that typed function contract.
11
+
12
+ The result is an LLM-backed function that can be reused like any other Python function,
13
+ while still being grounded in explicit tools, schemas, and config.
14
+
15
+ Tool definition and discovery live in
16
+ [`llm_function_tools`](https://pypi.org/project/llm-function-tools/).
17
+
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ pip install llm-function
23
+ ```
24
+