artha-engine 0.1.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.
- artha_engine/.DS_Store +0 -0
- artha_engine/__init__.py +241 -0
- artha_engine/app/__init__.py +71 -0
- artha_engine/app/actions.py +390 -0
- artha_engine/app/api_key_crypto.py +35 -0
- artha_engine/app/application.py +107 -0
- artha_engine/app/auth.py +195 -0
- artha_engine/app/authorization.py +42 -0
- artha_engine/app/build.py +179 -0
- artha_engine/app/cli.py +252 -0
- artha_engine/app/cli_config.py +72 -0
- artha_engine/app/config.py +126 -0
- artha_engine/app/context.py +26 -0
- artha_engine/app/http.py +234 -0
- artha_engine/app/managed_api_keys.py +224 -0
- artha_engine/app/mcp.py +324 -0
- artha_engine/app/remote_client.py +77 -0
- artha_engine/cli.py +380 -0
- artha_engine/decoders/__init__.py +79 -0
- artha_engine/decoders/base.py +16 -0
- artha_engine/decoders/core.py +122 -0
- artha_engine/decoders/llm.py +63 -0
- artha_engine/decoders/search.py +321 -0
- artha_engine/embeddings/__init__.py +33 -0
- artha_engine/embeddings/base.py +28 -0
- artha_engine/embeddings/deterministic.py +35 -0
- artha_engine/embeddings/fastembed.py +82 -0
- artha_engine/encoders/__init__.py +25 -0
- artha_engine/encoders/base.py +18 -0
- artha_engine/encoders/core.py +157 -0
- artha_engine/encoders/llm.py +57 -0
- artha_engine/lifecycle/__init__.py +16 -0
- artha_engine/lifecycle/base.py +32 -0
- artha_engine/lifecycle/core.py +79 -0
- artha_engine/llms/__init__.py +30 -0
- artha_engine/llms/base.py +20 -0
- artha_engine/llms/deterministic.py +69 -0
- artha_engine/llms/litellm.py +62 -0
- artha_engine/py.typed +0 -0
- artha_engine/representations/.DS_Store +0 -0
- artha_engine/representations/__init__.py +27 -0
- artha_engine/representations/arthaanu.py +102 -0
- artha_engine/retrieval/__init__.py +8 -0
- artha_engine/retrieval/bm25.py +74 -0
- artha_engine/retrieval/rrf.py +33 -0
- artha_engine/runtime/__init__.py +50 -0
- artha_engine/runtime/api/__init__.py +0 -0
- artha_engine/runtime/api/app.py +338 -0
- artha_engine/runtime/api/debug_context.py +22 -0
- artha_engine/runtime/api/schemas.py +168 -0
- artha_engine/runtime/auth.py +49 -0
- artha_engine/runtime/capabilities.py +37 -0
- artha_engine/runtime/component_registry.py +141 -0
- artha_engine/runtime/debug/__init__.py +19 -0
- artha_engine/runtime/debug/spans.py +122 -0
- artha_engine/runtime/engine.py +488 -0
- artha_engine/runtime/projections.py +19 -0
- artha_engine/runtime/registry.py +400 -0
- artha_engine/runtime/serde.py +16 -0
- artha_engine/store/__init__.py +42 -0
- artha_engine/store/api_key_store.py +296 -0
- artha_engine/store/api_keys_schema.sql +15 -0
- artha_engine/store/base.py +93 -0
- artha_engine/store/factory.py +39 -0
- artha_engine/store/memory.py +277 -0
- artha_engine/store/postgres.py +578 -0
- artha_engine/store/projection_sql.py +44 -0
- artha_engine/store/schema.sql +50 -0
- artha_engine/store/schema_postgres.sql +50 -0
- artha_engine/store/serde.py +24 -0
- artha_engine/store/sqlite.py +650 -0
- artha_engine-0.1.1.dist-info/METADATA +136 -0
- artha_engine-0.1.1.dist-info/RECORD +75 -0
- artha_engine-0.1.1.dist-info/WHEEL +4 -0
- artha_engine-0.1.1.dist-info/entry_points.txt +3 -0
artha_engine/.DS_Store
ADDED
|
Binary file
|
artha_engine/__init__.py
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
from artha_engine.decoders import (
|
|
2
|
+
BlobRefDecoder,
|
|
3
|
+
Bm25SearchDecoder,
|
|
4
|
+
Bm25SearchParams,
|
|
5
|
+
CollectionDecodeParams,
|
|
6
|
+
CollectionDecoder,
|
|
7
|
+
CosineSimilarityDecoder,
|
|
8
|
+
Decoder,
|
|
9
|
+
EmbeddingCandidate,
|
|
10
|
+
EmbeddingSearchHit,
|
|
11
|
+
EmbeddingSearchParams,
|
|
12
|
+
EmbeddingSearchResult,
|
|
13
|
+
FusedSearchHit,
|
|
14
|
+
GraphEdgeOutput,
|
|
15
|
+
JsonDecoder,
|
|
16
|
+
LLMArthaanuSummarizeDecoder,
|
|
17
|
+
LLMTextFormatDecoder,
|
|
18
|
+
LexicalMatchDecoder,
|
|
19
|
+
LexicalSearchHit,
|
|
20
|
+
LexicalSearchParams,
|
|
21
|
+
LexicalSearchResult,
|
|
22
|
+
NoDecoderParams,
|
|
23
|
+
QueryEmbeddingDecoder,
|
|
24
|
+
QueryEmbeddingParams,
|
|
25
|
+
ReciprocalRankFusionDecoder,
|
|
26
|
+
ReciprocalRankFusionParams,
|
|
27
|
+
RelationEdgeDecoder,
|
|
28
|
+
TextCandidate,
|
|
29
|
+
TextDecoder,
|
|
30
|
+
)
|
|
31
|
+
from artha_engine.encoders import (
|
|
32
|
+
BlobRefEncoder,
|
|
33
|
+
ChunkEmbeddingEncoder,
|
|
34
|
+
CollectionEncoder,
|
|
35
|
+
DeterministicEmbeddingEncoder,
|
|
36
|
+
Encoder,
|
|
37
|
+
GraphNodeEncoder,
|
|
38
|
+
JsonEncoder,
|
|
39
|
+
LLMJsonExtractEncoder,
|
|
40
|
+
RelationEncoder,
|
|
41
|
+
TextEncoder,
|
|
42
|
+
)
|
|
43
|
+
from artha_engine.lifecycle import (
|
|
44
|
+
CORE_LIFECYCLES_BY_NAME,
|
|
45
|
+
CollectionDedupeLifecycle,
|
|
46
|
+
JsonMergeLifecycle,
|
|
47
|
+
Lifecycle,
|
|
48
|
+
MultiLifecycle,
|
|
49
|
+
TextNormalizationLifecycle,
|
|
50
|
+
)
|
|
51
|
+
from artha_engine.representations import (
|
|
52
|
+
ARTHAANU_TYPES_BY_VALUE_TYPE,
|
|
53
|
+
Arthaanu,
|
|
54
|
+
BlobRefArthaanu,
|
|
55
|
+
CollectionArthaanu,
|
|
56
|
+
EmbeddingArthaanu,
|
|
57
|
+
GraphNodeArthaanu,
|
|
58
|
+
JsonArthaanu,
|
|
59
|
+
RelationArthaanu,
|
|
60
|
+
RelationValue,
|
|
61
|
+
SUPPORTED_VALUE_TYPES,
|
|
62
|
+
TextArthaanu,
|
|
63
|
+
)
|
|
64
|
+
from artha_engine.runtime import (
|
|
65
|
+
ArthaEngine,
|
|
66
|
+
AuthContext,
|
|
67
|
+
ComponentRegistry,
|
|
68
|
+
DEFAULT_DATABASE_URL,
|
|
69
|
+
DEFAULT_DB_PATH,
|
|
70
|
+
InMemoryArthaStore,
|
|
71
|
+
PostgresArthaStore,
|
|
72
|
+
SqliteArthaStore,
|
|
73
|
+
arthaanu_from_dict,
|
|
74
|
+
arthaanu_to_dict,
|
|
75
|
+
build_core_registry,
|
|
76
|
+
create_store,
|
|
77
|
+
default_store,
|
|
78
|
+
list_decoders,
|
|
79
|
+
list_encoders,
|
|
80
|
+
list_lifecycles,
|
|
81
|
+
list_projections,
|
|
82
|
+
normalize_auth_context,
|
|
83
|
+
normalize_profile_capabilities,
|
|
84
|
+
Projection,
|
|
85
|
+
ProfileCapability,
|
|
86
|
+
resolve_database_target,
|
|
87
|
+
)
|
|
88
|
+
from artha_engine.store import ArthaStore
|
|
89
|
+
from artha_engine.llms import (
|
|
90
|
+
DEFAULT_LLM,
|
|
91
|
+
DEFAULT_LLM_MODEL,
|
|
92
|
+
DeterministicTextGenerator,
|
|
93
|
+
LiteLLMTextGenerator,
|
|
94
|
+
TemplateTextGenerator,
|
|
95
|
+
TextGenerator,
|
|
96
|
+
default_llm,
|
|
97
|
+
)
|
|
98
|
+
from artha_engine.retrieval import Bm25Index, reciprocal_rank_fusion
|
|
99
|
+
from artha_engine.embeddings import (
|
|
100
|
+
DEFAULT_EMBEDDER,
|
|
101
|
+
DEFAULT_MODEL,
|
|
102
|
+
DeterministicTextEmbedder,
|
|
103
|
+
FastEmbedTextEmbedder,
|
|
104
|
+
NOMIC_DOCUMENT_PREFIX,
|
|
105
|
+
NOMIC_QUERY_PREFIX,
|
|
106
|
+
TextEmbedder,
|
|
107
|
+
default_embedder,
|
|
108
|
+
)
|
|
109
|
+
from artha_engine.app import (
|
|
110
|
+
ActionContext,
|
|
111
|
+
ActionDefinition,
|
|
112
|
+
ActionManifest,
|
|
113
|
+
ActionRegistry,
|
|
114
|
+
AnonymousAuthProvider,
|
|
115
|
+
ApiKeyAuthProvider,
|
|
116
|
+
ArthaApplication,
|
|
117
|
+
AuthProvider,
|
|
118
|
+
ClerkAuthProvider,
|
|
119
|
+
HttpExposure,
|
|
120
|
+
JwtAuthProvider,
|
|
121
|
+
McpHttpCompatibilityMiddleware,
|
|
122
|
+
ScopeAuthorizationPolicy,
|
|
123
|
+
action,
|
|
124
|
+
auth_provider_from_env,
|
|
125
|
+
streamable_http_app,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
__all__ = [
|
|
129
|
+
"ARTHAANU_TYPES_BY_VALUE_TYPE",
|
|
130
|
+
"Arthaanu",
|
|
131
|
+
"BlobRefArthaanu",
|
|
132
|
+
"CollectionArthaanu",
|
|
133
|
+
"TextArthaanu",
|
|
134
|
+
"EmbeddingArthaanu",
|
|
135
|
+
"JsonArthaanu",
|
|
136
|
+
"RelationArthaanu",
|
|
137
|
+
"RelationValue",
|
|
138
|
+
"SUPPORTED_VALUE_TYPES",
|
|
139
|
+
"GraphNodeArthaanu",
|
|
140
|
+
"BlobRefEncoder",
|
|
141
|
+
"ChunkEmbeddingEncoder",
|
|
142
|
+
"CollectionEncoder",
|
|
143
|
+
"DeterministicEmbeddingEncoder",
|
|
144
|
+
"Encoder",
|
|
145
|
+
"GraphNodeEncoder",
|
|
146
|
+
"JsonEncoder",
|
|
147
|
+
"LLMJsonExtractEncoder",
|
|
148
|
+
"RelationEncoder",
|
|
149
|
+
"TextEncoder",
|
|
150
|
+
"CORE_LIFECYCLES_BY_NAME",
|
|
151
|
+
"CollectionDedupeLifecycle",
|
|
152
|
+
"JsonMergeLifecycle",
|
|
153
|
+
"Lifecycle",
|
|
154
|
+
"MultiLifecycle",
|
|
155
|
+
"TextNormalizationLifecycle",
|
|
156
|
+
"Bm25Index",
|
|
157
|
+
"BlobRefDecoder",
|
|
158
|
+
"Bm25SearchDecoder",
|
|
159
|
+
"Bm25SearchParams",
|
|
160
|
+
"CollectionDecodeParams",
|
|
161
|
+
"CollectionDecoder",
|
|
162
|
+
"CosineSimilarityDecoder",
|
|
163
|
+
"Decoder",
|
|
164
|
+
"DEFAULT_LLM",
|
|
165
|
+
"DEFAULT_LLM_MODEL",
|
|
166
|
+
"DeterministicTextGenerator",
|
|
167
|
+
"EmbeddingCandidate",
|
|
168
|
+
"EmbeddingSearchHit",
|
|
169
|
+
"EmbeddingSearchParams",
|
|
170
|
+
"EmbeddingSearchResult",
|
|
171
|
+
"FusedSearchHit",
|
|
172
|
+
"GraphEdgeOutput",
|
|
173
|
+
"JsonDecoder",
|
|
174
|
+
"LLMArthaanuSummarizeDecoder",
|
|
175
|
+
"LLMJsonExtractEncoder",
|
|
176
|
+
"LLMTextFormatDecoder",
|
|
177
|
+
"LexicalMatchDecoder",
|
|
178
|
+
"LexicalSearchHit",
|
|
179
|
+
"LexicalSearchParams",
|
|
180
|
+
"LexicalSearchResult",
|
|
181
|
+
"LiteLLMTextGenerator",
|
|
182
|
+
"NoDecoderParams",
|
|
183
|
+
"QueryEmbeddingDecoder",
|
|
184
|
+
"QueryEmbeddingParams",
|
|
185
|
+
"ReciprocalRankFusionDecoder",
|
|
186
|
+
"ReciprocalRankFusionParams",
|
|
187
|
+
"RelationEdgeDecoder",
|
|
188
|
+
"TemplateTextGenerator",
|
|
189
|
+
"TextCandidate",
|
|
190
|
+
"TextDecoder",
|
|
191
|
+
"TextGenerator",
|
|
192
|
+
"default_llm",
|
|
193
|
+
"reciprocal_rank_fusion",
|
|
194
|
+
"ArthaEngine",
|
|
195
|
+
"ArthaStore",
|
|
196
|
+
"AuthContext",
|
|
197
|
+
"ComponentRegistry",
|
|
198
|
+
"DEFAULT_DATABASE_URL",
|
|
199
|
+
"DEFAULT_DB_PATH",
|
|
200
|
+
"InMemoryArthaStore",
|
|
201
|
+
"PostgresArthaStore",
|
|
202
|
+
"Projection",
|
|
203
|
+
"ProfileCapability",
|
|
204
|
+
"SqliteArthaStore",
|
|
205
|
+
"arthaanu_from_dict",
|
|
206
|
+
"arthaanu_to_dict",
|
|
207
|
+
"build_core_registry",
|
|
208
|
+
"create_store",
|
|
209
|
+
"default_store",
|
|
210
|
+
"resolve_database_target",
|
|
211
|
+
"list_decoders",
|
|
212
|
+
"list_encoders",
|
|
213
|
+
"list_lifecycles",
|
|
214
|
+
"list_projections",
|
|
215
|
+
"normalize_auth_context",
|
|
216
|
+
"normalize_profile_capabilities",
|
|
217
|
+
"DEFAULT_EMBEDDER",
|
|
218
|
+
"DEFAULT_MODEL",
|
|
219
|
+
"DeterministicTextEmbedder",
|
|
220
|
+
"FastEmbedTextEmbedder",
|
|
221
|
+
"NOMIC_DOCUMENT_PREFIX",
|
|
222
|
+
"NOMIC_QUERY_PREFIX",
|
|
223
|
+
"TextEmbedder",
|
|
224
|
+
"default_embedder",
|
|
225
|
+
"ActionContext",
|
|
226
|
+
"ActionDefinition",
|
|
227
|
+
"ActionManifest",
|
|
228
|
+
"ActionRegistry",
|
|
229
|
+
"AnonymousAuthProvider",
|
|
230
|
+
"ApiKeyAuthProvider",
|
|
231
|
+
"ArthaApplication",
|
|
232
|
+
"AuthProvider",
|
|
233
|
+
"ClerkAuthProvider",
|
|
234
|
+
"HttpExposure",
|
|
235
|
+
"JwtAuthProvider",
|
|
236
|
+
"McpHttpCompatibilityMiddleware",
|
|
237
|
+
"ScopeAuthorizationPolicy",
|
|
238
|
+
"action",
|
|
239
|
+
"auth_provider_from_env",
|
|
240
|
+
"streamable_http_app",
|
|
241
|
+
]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from artha_engine.app.actions import (
|
|
2
|
+
ActionDefinition,
|
|
3
|
+
ActionKind,
|
|
4
|
+
ActionManifest,
|
|
5
|
+
ActionRegistry,
|
|
6
|
+
CliExposure,
|
|
7
|
+
HttpExposure,
|
|
8
|
+
McpExposure,
|
|
9
|
+
action,
|
|
10
|
+
)
|
|
11
|
+
from artha_engine.app.application import ArthaApplication
|
|
12
|
+
from artha_engine.app.auth import (
|
|
13
|
+
AnonymousAuthProvider,
|
|
14
|
+
ApiKeyAuthProvider,
|
|
15
|
+
AuthenticationError,
|
|
16
|
+
AuthConfigurationError,
|
|
17
|
+
AuthProvider,
|
|
18
|
+
ClerkAuthProvider,
|
|
19
|
+
JwtAuthProvider,
|
|
20
|
+
auth_provider_from_env,
|
|
21
|
+
)
|
|
22
|
+
from artha_engine.app.managed_api_keys import (
|
|
23
|
+
CompositeAuthProvider,
|
|
24
|
+
ManagedApiKeyAuthProvider,
|
|
25
|
+
enable_managed_api_keys,
|
|
26
|
+
register_api_key_actions,
|
|
27
|
+
)
|
|
28
|
+
from artha_engine.app.authorization import (
|
|
29
|
+
AuthorizationDenied,
|
|
30
|
+
AuthorizationPolicy,
|
|
31
|
+
ScopeAuthorizationPolicy,
|
|
32
|
+
)
|
|
33
|
+
from artha_engine.app.context import ActionContext
|
|
34
|
+
from artha_engine.app.mcp import (
|
|
35
|
+
McpHttpCompatibilityMiddleware,
|
|
36
|
+
create_mcp_server,
|
|
37
|
+
run_mcp,
|
|
38
|
+
streamable_http_app,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"ActionContext",
|
|
43
|
+
"ActionDefinition",
|
|
44
|
+
"ActionKind",
|
|
45
|
+
"ActionManifest",
|
|
46
|
+
"ActionRegistry",
|
|
47
|
+
"AnonymousAuthProvider",
|
|
48
|
+
"ApiKeyAuthProvider",
|
|
49
|
+
"ArthaApplication",
|
|
50
|
+
"AuthenticationError",
|
|
51
|
+
"AuthConfigurationError",
|
|
52
|
+
"AuthProvider",
|
|
53
|
+
"AuthorizationDenied",
|
|
54
|
+
"AuthorizationPolicy",
|
|
55
|
+
"CliExposure",
|
|
56
|
+
"CompositeAuthProvider",
|
|
57
|
+
"ClerkAuthProvider",
|
|
58
|
+
"HttpExposure",
|
|
59
|
+
"JwtAuthProvider",
|
|
60
|
+
"ManagedApiKeyAuthProvider",
|
|
61
|
+
"McpExposure",
|
|
62
|
+
"McpHttpCompatibilityMiddleware",
|
|
63
|
+
"ScopeAuthorizationPolicy",
|
|
64
|
+
"action",
|
|
65
|
+
"auth_provider_from_env",
|
|
66
|
+
"create_mcp_server",
|
|
67
|
+
"enable_managed_api_keys",
|
|
68
|
+
"register_api_key_actions",
|
|
69
|
+
"run_mcp",
|
|
70
|
+
"streamable_http_app",
|
|
71
|
+
]
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import re
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections.abc import Callable, Iterable
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from types import ModuleType, UnionType
|
|
9
|
+
from typing import Any, Literal, get_args, get_origin, get_type_hints
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, ConfigDict, Field, TypeAdapter, create_model
|
|
12
|
+
|
|
13
|
+
from artha_engine.app.context import ActionContext
|
|
14
|
+
from artha_engine.runtime.auth import AuthContext
|
|
15
|
+
from artha_engine.runtime.engine import ArthaEngine
|
|
16
|
+
|
|
17
|
+
ActionKind = Literal["read", "write", "destructive"]
|
|
18
|
+
_ACTION_ATTRIBUTE = "__artha_action_spec__"
|
|
19
|
+
_NAME_PATTERN = re.compile(r"^[a-z][a-z0-9_.-]*$")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class HttpExposure:
|
|
24
|
+
method: str
|
|
25
|
+
path: str
|
|
26
|
+
status_code: int = 200
|
|
27
|
+
|
|
28
|
+
def __post_init__(self) -> None:
|
|
29
|
+
method = self.method.upper()
|
|
30
|
+
if method not in {"GET", "POST", "PUT", "PATCH", "DELETE"}:
|
|
31
|
+
raise ValueError(f"Unsupported action HTTP method: {self.method}")
|
|
32
|
+
if not self.path.startswith("/"):
|
|
33
|
+
raise ValueError("Action HTTP paths must begin with /")
|
|
34
|
+
object.__setattr__(self, "method", method)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class McpExposure:
|
|
39
|
+
name: str
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class CliExposure:
|
|
44
|
+
command: tuple[str, ...]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class ActionSpec:
|
|
49
|
+
name: str
|
|
50
|
+
description: str | None = None
|
|
51
|
+
version: str = "1"
|
|
52
|
+
kind: ActionKind = "read"
|
|
53
|
+
scopes: tuple[str, ...] = ()
|
|
54
|
+
public: bool = False
|
|
55
|
+
http: HttpExposure | None = None
|
|
56
|
+
mcp: McpExposure | None = None
|
|
57
|
+
cli: CliExposure | None = None
|
|
58
|
+
inject: dict[str, str] = field(default_factory=dict)
|
|
59
|
+
tags: tuple[str, ...] = ()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ActionManifest(BaseModel):
|
|
63
|
+
model_config = ConfigDict(frozen=True)
|
|
64
|
+
|
|
65
|
+
name: str
|
|
66
|
+
description: str
|
|
67
|
+
version: str
|
|
68
|
+
kind: ActionKind
|
|
69
|
+
scopes: list[str] = Field(default_factory=list)
|
|
70
|
+
public: bool
|
|
71
|
+
input_schema: dict[str, Any]
|
|
72
|
+
output_schema: dict[str, Any]
|
|
73
|
+
http: dict[str, Any] | None = None
|
|
74
|
+
mcp: dict[str, Any] | None = None
|
|
75
|
+
cli: dict[str, Any] | None = None
|
|
76
|
+
tags: list[str] = Field(default_factory=list)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True)
|
|
80
|
+
class ActionDefinition:
|
|
81
|
+
spec: ActionSpec
|
|
82
|
+
function: Callable[..., Any]
|
|
83
|
+
input_model: type[BaseModel]
|
|
84
|
+
return_annotation: Any
|
|
85
|
+
public_parameters: tuple[str, ...]
|
|
86
|
+
model_parameter: str | None
|
|
87
|
+
injected_parameters: dict[str, str]
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def name(self) -> str:
|
|
91
|
+
return self.spec.name
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def description(self) -> str:
|
|
95
|
+
return self.spec.description or inspect.getdoc(self.function) or self.name
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def kind(self) -> ActionKind:
|
|
99
|
+
return self.spec.kind
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def scopes(self) -> tuple[str, ...]:
|
|
103
|
+
return self.spec.scopes
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def public(self) -> bool:
|
|
107
|
+
return self.spec.public
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def http(self) -> HttpExposure | None:
|
|
111
|
+
return self.spec.http
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def mcp(self) -> McpExposure | None:
|
|
115
|
+
return self.spec.mcp
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def cli(self) -> CliExposure | None:
|
|
119
|
+
return self.spec.cli
|
|
120
|
+
|
|
121
|
+
def manifest(self) -> ActionManifest:
|
|
122
|
+
output_schema: dict[str, Any] = {}
|
|
123
|
+
if self.return_annotation not in {Any, inspect.Signature.empty, None}:
|
|
124
|
+
output_schema = TypeAdapter(self.return_annotation).json_schema()
|
|
125
|
+
return ActionManifest(
|
|
126
|
+
name=self.name,
|
|
127
|
+
description=self.description,
|
|
128
|
+
version=self.spec.version,
|
|
129
|
+
kind=self.kind,
|
|
130
|
+
scopes=list(self.scopes),
|
|
131
|
+
public=self.public,
|
|
132
|
+
input_schema=self.input_model.model_json_schema(),
|
|
133
|
+
output_schema=output_schema,
|
|
134
|
+
http=(
|
|
135
|
+
{
|
|
136
|
+
"method": self.http.method,
|
|
137
|
+
"path": self.http.path,
|
|
138
|
+
"status_code": self.http.status_code,
|
|
139
|
+
}
|
|
140
|
+
if self.http
|
|
141
|
+
else None
|
|
142
|
+
),
|
|
143
|
+
mcp={"name": self.mcp.name} if self.mcp else None,
|
|
144
|
+
cli={"command": list(self.cli.command)} if self.cli else None,
|
|
145
|
+
tags=list(self.spec.tags),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def build_arguments(
|
|
149
|
+
self,
|
|
150
|
+
payload: BaseModel | dict[str, Any] | None,
|
|
151
|
+
context: ActionContext,
|
|
152
|
+
) -> dict[str, Any]:
|
|
153
|
+
validated = (
|
|
154
|
+
payload
|
|
155
|
+
if isinstance(payload, self.input_model)
|
|
156
|
+
else self.input_model.model_validate(payload or {})
|
|
157
|
+
)
|
|
158
|
+
arguments: dict[str, Any] = {}
|
|
159
|
+
if self.model_parameter is not None:
|
|
160
|
+
arguments[self.model_parameter] = validated
|
|
161
|
+
else:
|
|
162
|
+
for name in self.public_parameters:
|
|
163
|
+
arguments[name] = getattr(validated, name)
|
|
164
|
+
|
|
165
|
+
hints = get_type_hints(self.function)
|
|
166
|
+
for name, parameter in inspect.signature(self.function).parameters.items():
|
|
167
|
+
annotation = hints.get(name, parameter.annotation)
|
|
168
|
+
if annotation is ArthaEngine:
|
|
169
|
+
arguments[name] = context.engine
|
|
170
|
+
elif annotation is ActionContext:
|
|
171
|
+
arguments[name] = context
|
|
172
|
+
elif annotation is AuthContext:
|
|
173
|
+
arguments[name] = context.auth
|
|
174
|
+
elif name in self.injected_parameters:
|
|
175
|
+
arguments[name] = context.require_service(self.injected_parameters[name])
|
|
176
|
+
return arguments
|
|
177
|
+
|
|
178
|
+
async def invoke(
|
|
179
|
+
self,
|
|
180
|
+
payload: BaseModel | dict[str, Any] | None,
|
|
181
|
+
context: ActionContext,
|
|
182
|
+
) -> Any:
|
|
183
|
+
arguments = self.build_arguments(payload, context)
|
|
184
|
+
if inspect.iscoroutinefunction(self.function):
|
|
185
|
+
result = self.function(**arguments)
|
|
186
|
+
else:
|
|
187
|
+
result = await asyncio.to_thread(self.function, **arguments)
|
|
188
|
+
if inspect.isawaitable(result):
|
|
189
|
+
result = await result
|
|
190
|
+
if self.return_annotation not in {Any, inspect.Signature.empty, None}:
|
|
191
|
+
result = TypeAdapter(self.return_annotation).validate_python(result)
|
|
192
|
+
return result
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class ActionRegistry:
|
|
196
|
+
"""Typed application functions exposed through HTTP, MCP, and CLI."""
|
|
197
|
+
|
|
198
|
+
def __init__(self) -> None:
|
|
199
|
+
self._actions: dict[str, ActionDefinition] = {}
|
|
200
|
+
|
|
201
|
+
def action(self, **kwargs: Any) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
202
|
+
def decorator(function: Callable[..., Any]) -> Callable[..., Any]:
|
|
203
|
+
decorated = action(**kwargs)(function)
|
|
204
|
+
self.register(decorated)
|
|
205
|
+
return decorated
|
|
206
|
+
|
|
207
|
+
return decorator
|
|
208
|
+
|
|
209
|
+
def register(self, function: Callable[..., Any]) -> ActionDefinition:
|
|
210
|
+
spec = getattr(function, _ACTION_ATTRIBUTE, None)
|
|
211
|
+
if not isinstance(spec, ActionSpec):
|
|
212
|
+
raise ValueError(f"Function is not decorated with @action: {function.__name__}")
|
|
213
|
+
if spec.name in self._actions:
|
|
214
|
+
raise ValueError(f"Action is already registered: {spec.name}")
|
|
215
|
+
definition = _build_definition(function, spec)
|
|
216
|
+
self._actions[spec.name] = definition
|
|
217
|
+
return definition
|
|
218
|
+
|
|
219
|
+
def discover(self, module: ModuleType) -> list[ActionDefinition]:
|
|
220
|
+
discovered: list[ActionDefinition] = []
|
|
221
|
+
for value in vars(module).values():
|
|
222
|
+
if callable(value) and isinstance(getattr(value, _ACTION_ATTRIBUTE, None), ActionSpec):
|
|
223
|
+
if getattr(value, _ACTION_ATTRIBUTE).name not in self._actions:
|
|
224
|
+
discovered.append(self.register(value))
|
|
225
|
+
return discovered
|
|
226
|
+
|
|
227
|
+
def get(self, name: str) -> ActionDefinition:
|
|
228
|
+
try:
|
|
229
|
+
return self._actions[name]
|
|
230
|
+
except KeyError as exc:
|
|
231
|
+
raise KeyError(f"Unknown action: {name}") from exc
|
|
232
|
+
|
|
233
|
+
def list(self) -> list[ActionDefinition]:
|
|
234
|
+
return [self._actions[name] for name in sorted(self._actions)]
|
|
235
|
+
|
|
236
|
+
def manifests(self) -> list[dict[str, Any]]:
|
|
237
|
+
return [item.manifest().model_dump(mode="json") for item in self.list()]
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def action(
|
|
241
|
+
*,
|
|
242
|
+
name: str,
|
|
243
|
+
description: str | None = None,
|
|
244
|
+
version: str = "1",
|
|
245
|
+
kind: ActionKind = "read",
|
|
246
|
+
scopes: Iterable[str] = (),
|
|
247
|
+
public: bool = False,
|
|
248
|
+
http: bool | str | tuple[str, str] | HttpExposure = True,
|
|
249
|
+
mcp: bool | str | McpExposure = True,
|
|
250
|
+
cli: bool | str | Iterable[str] | CliExposure = True,
|
|
251
|
+
inject: Iterable[str] | dict[str, str] = (),
|
|
252
|
+
tags: Iterable[str] = (),
|
|
253
|
+
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
254
|
+
"""Mark a typed function as an Artha application action."""
|
|
255
|
+
|
|
256
|
+
if not _NAME_PATTERN.fullmatch(name):
|
|
257
|
+
raise ValueError(
|
|
258
|
+
"Action names must start with a lowercase letter and use lowercase "
|
|
259
|
+
"letters, numbers, dots, dashes, or underscores"
|
|
260
|
+
)
|
|
261
|
+
if kind not in {"read", "write", "destructive"}:
|
|
262
|
+
raise ValueError(f"Unsupported action kind: {kind}")
|
|
263
|
+
spec = ActionSpec(
|
|
264
|
+
name=name,
|
|
265
|
+
description=description,
|
|
266
|
+
version=version,
|
|
267
|
+
kind=kind,
|
|
268
|
+
scopes=tuple(scopes),
|
|
269
|
+
public=public,
|
|
270
|
+
http=_normalize_http(name, http),
|
|
271
|
+
mcp=_normalize_mcp(name, mcp),
|
|
272
|
+
cli=_normalize_cli(name, cli),
|
|
273
|
+
inject=_normalize_injections(inject),
|
|
274
|
+
tags=tuple(tags),
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
def decorator(function: Callable[..., Any]) -> Callable[..., Any]:
|
|
278
|
+
if hasattr(function, _ACTION_ATTRIBUTE):
|
|
279
|
+
raise ValueError(f"Function already has an Artha action: {function.__name__}")
|
|
280
|
+
setattr(function, _ACTION_ATTRIBUTE, spec)
|
|
281
|
+
return function
|
|
282
|
+
|
|
283
|
+
return decorator
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _build_definition(function: Callable[..., Any], spec: ActionSpec) -> ActionDefinition:
|
|
287
|
+
signature = inspect.signature(function)
|
|
288
|
+
hints = get_type_hints(function)
|
|
289
|
+
public: list[tuple[str, inspect.Parameter, Any]] = []
|
|
290
|
+
injected: dict[str, str] = dict(spec.inject)
|
|
291
|
+
|
|
292
|
+
for name, parameter in signature.parameters.items():
|
|
293
|
+
annotation = hints.get(name, parameter.annotation)
|
|
294
|
+
if annotation in {ArthaEngine, ActionContext, AuthContext}:
|
|
295
|
+
continue
|
|
296
|
+
if name in injected:
|
|
297
|
+
continue
|
|
298
|
+
if annotation is inspect.Signature.empty:
|
|
299
|
+
raise TypeError(f"Action parameter {spec.name}.{name} must have a type annotation")
|
|
300
|
+
public.append((name, parameter, annotation))
|
|
301
|
+
|
|
302
|
+
model_parameter: str | None = None
|
|
303
|
+
if len(public) == 1 and _is_model_type(public[0][2]):
|
|
304
|
+
model_parameter = public[0][0]
|
|
305
|
+
input_model = public[0][2]
|
|
306
|
+
public_names: tuple[str, ...] = ()
|
|
307
|
+
else:
|
|
308
|
+
fields: dict[str, tuple[Any, Any]] = {}
|
|
309
|
+
for name, parameter, annotation in public:
|
|
310
|
+
default: Any = ...
|
|
311
|
+
if parameter.default is not inspect.Signature.empty:
|
|
312
|
+
default = parameter.default
|
|
313
|
+
fields[name] = (annotation, default)
|
|
314
|
+
model_name = "".join(part.title() for part in re.split(r"[_.-]", spec.name)) + "Input"
|
|
315
|
+
input_model = create_model(model_name, **fields)
|
|
316
|
+
public_names = tuple(name for name, _, _ in public)
|
|
317
|
+
|
|
318
|
+
return ActionDefinition(
|
|
319
|
+
spec=spec,
|
|
320
|
+
function=function,
|
|
321
|
+
input_model=input_model,
|
|
322
|
+
return_annotation=hints.get("return", signature.return_annotation),
|
|
323
|
+
public_parameters=public_names,
|
|
324
|
+
model_parameter=model_parameter,
|
|
325
|
+
injected_parameters=injected,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _normalize_http(
|
|
330
|
+
name: str,
|
|
331
|
+
value: bool | str | tuple[str, str] | HttpExposure,
|
|
332
|
+
) -> HttpExposure | None:
|
|
333
|
+
if value is False:
|
|
334
|
+
return None
|
|
335
|
+
if isinstance(value, HttpExposure):
|
|
336
|
+
return value
|
|
337
|
+
if value is True:
|
|
338
|
+
return HttpExposure("POST", f"/actions/{name.replace('.', '/')}")
|
|
339
|
+
if isinstance(value, tuple):
|
|
340
|
+
return HttpExposure(value[0], value[1])
|
|
341
|
+
method, separator, path = value.partition(" ")
|
|
342
|
+
if not separator:
|
|
343
|
+
return HttpExposure("POST", value)
|
|
344
|
+
return HttpExposure(method, path)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _normalize_mcp(name: str, value: bool | str | McpExposure) -> McpExposure | None:
|
|
348
|
+
if value is False:
|
|
349
|
+
return None
|
|
350
|
+
if isinstance(value, McpExposure):
|
|
351
|
+
return value
|
|
352
|
+
if isinstance(value, str):
|
|
353
|
+
return McpExposure(value)
|
|
354
|
+
return McpExposure(re.sub(r"[^a-zA-Z0-9_]", "_", name))
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _normalize_cli(
|
|
358
|
+
name: str,
|
|
359
|
+
value: bool | str | Iterable[str] | CliExposure,
|
|
360
|
+
) -> CliExposure | None:
|
|
361
|
+
if value is False:
|
|
362
|
+
return None
|
|
363
|
+
if isinstance(value, CliExposure):
|
|
364
|
+
return value
|
|
365
|
+
if value is True:
|
|
366
|
+
return CliExposure(tuple(part for part in re.split(r"[.\s]+", name) if part))
|
|
367
|
+
if isinstance(value, str):
|
|
368
|
+
return CliExposure(tuple(part for part in value.split() if part))
|
|
369
|
+
return CliExposure(tuple(value))
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _normalize_injections(value: Iterable[str] | dict[str, str]) -> dict[str, str]:
|
|
373
|
+
if isinstance(value, dict):
|
|
374
|
+
return dict(value)
|
|
375
|
+
return {name: name for name in value}
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _is_model_type(annotation: Any) -> bool:
|
|
379
|
+
return inspect.isclass(annotation) and issubclass(annotation, BaseModel)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def unwrap_optional(annotation: Any) -> tuple[Any, bool]:
|
|
383
|
+
origin = get_origin(annotation)
|
|
384
|
+
if origin not in {UnionType, getattr(__import__("typing"), "Union")}:
|
|
385
|
+
return annotation, False
|
|
386
|
+
args = get_args(annotation)
|
|
387
|
+
without_none = tuple(arg for arg in args if arg is not type(None))
|
|
388
|
+
if len(without_none) == 1 and len(without_none) != len(args):
|
|
389
|
+
return without_none[0], True
|
|
390
|
+
return annotation, False
|