aethergraph 0.1.0a1__py3-none-any.whl → 0.1.0a2__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.
- aethergraph/__init__.py +4 -10
- aethergraph/__main__.py +293 -0
- aethergraph/api/v1/__init__.py +0 -0
- aethergraph/api/v1/agents.py +46 -0
- aethergraph/api/v1/apps.py +70 -0
- aethergraph/api/v1/artifacts.py +415 -0
- aethergraph/api/v1/channels.py +89 -0
- aethergraph/api/v1/deps.py +168 -0
- aethergraph/api/v1/graphs.py +259 -0
- aethergraph/api/v1/identity.py +25 -0
- aethergraph/api/v1/memory.py +353 -0
- aethergraph/api/v1/misc.py +47 -0
- aethergraph/api/v1/pagination.py +29 -0
- aethergraph/api/v1/runs.py +568 -0
- aethergraph/api/v1/schemas.py +535 -0
- aethergraph/api/v1/session.py +323 -0
- aethergraph/api/v1/stats.py +201 -0
- aethergraph/api/v1/viz.py +152 -0
- aethergraph/config/config.py +22 -0
- aethergraph/config/loader.py +3 -2
- aethergraph/config/storage.py +209 -0
- aethergraph/contracts/__init__.py +0 -0
- aethergraph/contracts/services/__init__.py +0 -0
- aethergraph/contracts/services/artifacts.py +27 -14
- aethergraph/contracts/services/memory.py +45 -17
- aethergraph/contracts/services/metering.py +129 -0
- aethergraph/contracts/services/runs.py +50 -0
- aethergraph/contracts/services/sessions.py +87 -0
- aethergraph/contracts/services/state_stores.py +3 -0
- aethergraph/contracts/services/viz.py +44 -0
- aethergraph/contracts/storage/artifact_index.py +88 -0
- aethergraph/contracts/storage/artifact_store.py +99 -0
- aethergraph/contracts/storage/async_kv.py +34 -0
- aethergraph/contracts/storage/blob_store.py +50 -0
- aethergraph/contracts/storage/doc_store.py +35 -0
- aethergraph/contracts/storage/event_log.py +31 -0
- aethergraph/contracts/storage/vector_index.py +48 -0
- aethergraph/core/__init__.py +0 -0
- aethergraph/core/execution/forward_scheduler.py +13 -2
- aethergraph/core/execution/global_scheduler.py +21 -15
- aethergraph/core/execution/step_forward.py +10 -1
- aethergraph/core/graph/__init__.py +0 -0
- aethergraph/core/graph/graph_builder.py +8 -4
- aethergraph/core/graph/graph_fn.py +156 -15
- aethergraph/core/graph/graph_spec.py +8 -0
- aethergraph/core/graph/graphify.py +146 -27
- aethergraph/core/graph/node_spec.py +0 -2
- aethergraph/core/graph/node_state.py +3 -0
- aethergraph/core/graph/task_graph.py +39 -1
- aethergraph/core/runtime/__init__.py +0 -0
- aethergraph/core/runtime/ad_hoc_context.py +64 -4
- aethergraph/core/runtime/base_service.py +28 -4
- aethergraph/core/runtime/execution_context.py +13 -15
- aethergraph/core/runtime/graph_runner.py +222 -37
- aethergraph/core/runtime/node_context.py +510 -6
- aethergraph/core/runtime/node_services.py +12 -5
- aethergraph/core/runtime/recovery.py +15 -1
- aethergraph/core/runtime/run_manager.py +783 -0
- aethergraph/core/runtime/run_manager_local.py +204 -0
- aethergraph/core/runtime/run_registration.py +2 -2
- aethergraph/core/runtime/run_types.py +89 -0
- aethergraph/core/runtime/runtime_env.py +136 -7
- aethergraph/core/runtime/runtime_metering.py +71 -0
- aethergraph/core/runtime/runtime_registry.py +36 -13
- aethergraph/core/runtime/runtime_services.py +194 -6
- aethergraph/core/tools/builtins/toolset.py +1 -1
- aethergraph/core/tools/toolkit.py +5 -0
- aethergraph/plugins/agents/default_chat_agent copy.py +90 -0
- aethergraph/plugins/agents/default_chat_agent.py +171 -0
- aethergraph/plugins/agents/shared.py +81 -0
- aethergraph/plugins/channel/adapters/webui.py +112 -112
- aethergraph/plugins/channel/routes/webui_routes.py +367 -102
- aethergraph/plugins/channel/utils/slack_utils.py +115 -59
- aethergraph/plugins/channel/utils/telegram_utils.py +88 -47
- aethergraph/plugins/channel/websockets/weibui_ws.py +172 -0
- aethergraph/runtime/__init__.py +15 -0
- aethergraph/server/app_factory.py +190 -34
- aethergraph/server/clients/channel_client.py +202 -0
- aethergraph/server/http/channel_http_routes.py +116 -0
- aethergraph/server/http/channel_ws_routers.py +45 -0
- aethergraph/server/loading.py +117 -0
- aethergraph/server/server.py +131 -0
- aethergraph/server/server_state.py +240 -0
- aethergraph/server/start.py +227 -66
- aethergraph/server/ui_static/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- aethergraph/server/ui_static/assets/index-BR5GtXcZ.css +1 -0
- aethergraph/server/ui_static/assets/index-CQ0HZZ83.js +400 -0
- aethergraph/server/ui_static/index.html +15 -0
- aethergraph/server/ui_static/logo.png +0 -0
- aethergraph/services/artifacts/__init__.py +0 -0
- aethergraph/services/artifacts/facade.py +1239 -132
- aethergraph/services/auth/{dev.py → authn.py} +0 -8
- aethergraph/services/auth/authz.py +100 -0
- aethergraph/services/channel/__init__.py +0 -0
- aethergraph/services/channel/channel_bus.py +19 -1
- aethergraph/services/channel/factory.py +13 -1
- aethergraph/services/channel/ingress.py +311 -0
- aethergraph/services/channel/queue_adapter.py +75 -0
- aethergraph/services/channel/session.py +502 -19
- aethergraph/services/container/default_container.py +122 -43
- aethergraph/services/continuations/continuation.py +6 -0
- aethergraph/services/continuations/stores/fs_store.py +19 -0
- aethergraph/services/eventhub/event_hub.py +76 -0
- aethergraph/services/kv/__init__.py +0 -0
- aethergraph/services/kv/ephemeral.py +244 -0
- aethergraph/services/llm/__init__.py +0 -0
- aethergraph/services/llm/generic_client copy.py +691 -0
- aethergraph/services/llm/generic_client.py +1288 -187
- aethergraph/services/llm/providers.py +3 -1
- aethergraph/services/llm/types.py +47 -0
- aethergraph/services/llm/utils.py +284 -0
- aethergraph/services/logger/std.py +3 -0
- aethergraph/services/mcp/__init__.py +9 -0
- aethergraph/services/mcp/http_client.py +38 -0
- aethergraph/services/mcp/service.py +225 -1
- aethergraph/services/mcp/stdio_client.py +41 -6
- aethergraph/services/mcp/ws_client.py +44 -2
- aethergraph/services/memory/__init__.py +0 -0
- aethergraph/services/memory/distillers/llm_long_term.py +234 -0
- aethergraph/services/memory/distillers/llm_meta_summary.py +398 -0
- aethergraph/services/memory/distillers/long_term.py +225 -0
- aethergraph/services/memory/facade/__init__.py +3 -0
- aethergraph/services/memory/facade/chat.py +440 -0
- aethergraph/services/memory/facade/core.py +447 -0
- aethergraph/services/memory/facade/distillation.py +424 -0
- aethergraph/services/memory/facade/rag.py +410 -0
- aethergraph/services/memory/facade/results.py +315 -0
- aethergraph/services/memory/facade/retrieval.py +139 -0
- aethergraph/services/memory/facade/types.py +77 -0
- aethergraph/services/memory/facade/utils.py +43 -0
- aethergraph/services/memory/facade_dep.py +1539 -0
- aethergraph/services/memory/factory.py +9 -3
- aethergraph/services/memory/utils.py +10 -0
- aethergraph/services/metering/eventlog_metering.py +470 -0
- aethergraph/services/metering/noop.py +25 -4
- aethergraph/services/rag/__init__.py +0 -0
- aethergraph/services/rag/facade.py +279 -23
- aethergraph/services/rag/index_factory.py +2 -2
- aethergraph/services/rag/node_rag.py +317 -0
- aethergraph/services/rate_limit/inmem_rate_limit.py +24 -0
- aethergraph/services/registry/__init__.py +0 -0
- aethergraph/services/registry/agent_app_meta.py +419 -0
- aethergraph/services/registry/registry_key.py +1 -1
- aethergraph/services/registry/unified_registry.py +74 -6
- aethergraph/services/scope/scope.py +159 -0
- aethergraph/services/scope/scope_factory.py +164 -0
- aethergraph/services/state_stores/serialize.py +5 -0
- aethergraph/services/state_stores/utils.py +2 -1
- aethergraph/services/viz/__init__.py +0 -0
- aethergraph/services/viz/facade.py +413 -0
- aethergraph/services/viz/viz_service.py +69 -0
- aethergraph/storage/artifacts/artifact_index_jsonl.py +180 -0
- aethergraph/storage/artifacts/artifact_index_sqlite.py +426 -0
- aethergraph/storage/artifacts/cas_store.py +422 -0
- aethergraph/storage/artifacts/fs_cas.py +18 -0
- aethergraph/storage/artifacts/s3_cas.py +14 -0
- aethergraph/storage/artifacts/utils.py +124 -0
- aethergraph/storage/blob/fs_blob.py +86 -0
- aethergraph/storage/blob/s3_blob.py +115 -0
- aethergraph/storage/continuation_store/fs_cont.py +283 -0
- aethergraph/storage/continuation_store/inmem_cont.py +146 -0
- aethergraph/storage/continuation_store/kvdoc_cont.py +261 -0
- aethergraph/storage/docstore/fs_doc.py +63 -0
- aethergraph/storage/docstore/sqlite_doc.py +31 -0
- aethergraph/storage/docstore/sqlite_doc_sync.py +90 -0
- aethergraph/storage/eventlog/fs_event.py +136 -0
- aethergraph/storage/eventlog/sqlite_event.py +47 -0
- aethergraph/storage/eventlog/sqlite_event_sync.py +178 -0
- aethergraph/storage/factory.py +432 -0
- aethergraph/storage/fs_utils.py +28 -0
- aethergraph/storage/graph_state_store/state_store.py +64 -0
- aethergraph/storage/kv/inmem_kv.py +103 -0
- aethergraph/storage/kv/layered_kv.py +52 -0
- aethergraph/storage/kv/sqlite_kv.py +39 -0
- aethergraph/storage/kv/sqlite_kv_sync.py +98 -0
- aethergraph/storage/memory/event_persist.py +68 -0
- aethergraph/storage/memory/fs_persist.py +118 -0
- aethergraph/{services/memory/hotlog_kv.py → storage/memory/hotlog.py} +8 -2
- aethergraph/{services → storage}/memory/indices.py +31 -7
- aethergraph/storage/metering/meter_event.py +55 -0
- aethergraph/storage/runs/doc_store.py +280 -0
- aethergraph/storage/runs/inmen_store.py +82 -0
- aethergraph/storage/runs/sqlite_run_store.py +403 -0
- aethergraph/storage/sessions/doc_store.py +183 -0
- aethergraph/storage/sessions/inmem_store.py +110 -0
- aethergraph/storage/sessions/sqlite_session_store.py +399 -0
- aethergraph/storage/vector_index/chroma_index.py +138 -0
- aethergraph/storage/vector_index/faiss_index.py +179 -0
- aethergraph/storage/vector_index/sqlite_index.py +187 -0
- {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a2.dist-info}/METADATA +138 -31
- aethergraph-0.1.0a2.dist-info/RECORD +356 -0
- aethergraph-0.1.0a2.dist-info/entry_points.txt +3 -0
- aethergraph/services/artifacts/factory.py +0 -35
- aethergraph/services/artifacts/fs_store.py +0 -656
- aethergraph/services/artifacts/jsonl_index.py +0 -123
- aethergraph/services/artifacts/sqlite_index.py +0 -209
- aethergraph/services/memory/distillers/episode.py +0 -116
- aethergraph/services/memory/distillers/rolling.py +0 -74
- aethergraph/services/memory/facade.py +0 -633
- aethergraph/services/memory/persist_fs.py +0 -40
- aethergraph/services/rag/index/base.py +0 -27
- aethergraph/services/rag/index/faiss_index.py +0 -121
- aethergraph/services/rag/index/sqlite_index.py +0 -134
- aethergraph-0.1.0a1.dist-info/RECORD +0 -182
- aethergraph-0.1.0a1.dist-info/entry_points.txt +0 -2
- {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a2.dist-info}/WHEEL +0 -0
- {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a2.dist-info}/licenses/LICENSE +0 -0
- {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a2.dist-info}/licenses/NOTICE +0 -0
- {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any, Literal
|
|
3
|
+
|
|
4
|
+
ChatOutputFormat = Literal["text", "json_object", "json_schema"]
|
|
5
|
+
|
|
6
|
+
ImageFormat = Literal["png", "jpeg", "webp"]
|
|
7
|
+
ImageResponseFormat = Literal["b64_json", "url"] # url only for dall-e models typically
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class JsonSchemaSpec:
|
|
12
|
+
name: str
|
|
13
|
+
schema: dict[str, Any]
|
|
14
|
+
strict: bool = True
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class ImageInput:
|
|
19
|
+
data: bytes | None = None
|
|
20
|
+
b64: str | None = None # base64 without data: prefix
|
|
21
|
+
mime_type: str | None = None
|
|
22
|
+
url: str | None = None # http(s) url OR provider file_uri
|
|
23
|
+
is_file_uri: bool = False # Gemini file URIs
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class LLMUnsupportedFeatureError(RuntimeError):
|
|
27
|
+
def __init__(self, provider: str, model: str | None, feature: str, detail: str | None = None):
|
|
28
|
+
msg = f"Provider '{provider}' / model '{model or '?'}' does not support: {feature}"
|
|
29
|
+
if detail:
|
|
30
|
+
msg += f" ({detail})"
|
|
31
|
+
super().__init__(msg)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class GeneratedImage:
|
|
36
|
+
# Exactly one of these is typically present.
|
|
37
|
+
b64: str | None = None
|
|
38
|
+
url: str | None = None
|
|
39
|
+
mime_type: str | None = None
|
|
40
|
+
revised_prompt: str | None = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class ImageGenerationResult:
|
|
45
|
+
images: list[GeneratedImage]
|
|
46
|
+
usage: dict[str, int] # often empty for image endpoints
|
|
47
|
+
raw: dict[str, Any] | None = None
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
from aethergraph.services.llm.types import ImageInput
|
|
8
|
+
|
|
9
|
+
ChatOutputFormat = Literal["text", "json_object", "json_schema"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _is_data_url(s: str) -> bool:
|
|
13
|
+
return isinstance(s, str) and s.startswith("data:") and ";base64," in s
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _image_bytes_to_b64(data: bytes) -> str:
|
|
17
|
+
return base64.b64encode(data).decode("ascii")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _data_url_to_b64_and_mime(url: str) -> tuple[str, str]:
|
|
21
|
+
head, b64 = url.split(",", 1)
|
|
22
|
+
mime = head.split(";")[0].split(":", 1)[1]
|
|
23
|
+
return b64, mime
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _ensure_b64(img: ImageInput) -> tuple[str, str]:
|
|
27
|
+
if img.b64 and img.mime_type:
|
|
28
|
+
return img.b64, img.mime_type
|
|
29
|
+
if img.url and _is_data_url(img.url):
|
|
30
|
+
return _data_url_to_b64_and_mime(img.url)
|
|
31
|
+
if img.data and img.mime_type:
|
|
32
|
+
import base64
|
|
33
|
+
|
|
34
|
+
return base64.b64encode(img.data).decode("ascii"), img.mime_type
|
|
35
|
+
raise ValueError("ImageInput must have (b64+mime_type) or (data+mime_type) or a data: URL")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _normalize_messages(messages: Sequence[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
39
|
+
"""
|
|
40
|
+
Normalize many common message shapes into:
|
|
41
|
+
{"role": "...", "parts": [{"type":"text","text":...} | {"type":"image","image": ImageInput}]}
|
|
42
|
+
Supports:
|
|
43
|
+
- {"role","content":"text"}
|
|
44
|
+
- ChatCompletions multimodal image_url parts
|
|
45
|
+
- Responses input_image parts
|
|
46
|
+
- Anthropic image source blocks
|
|
47
|
+
"""
|
|
48
|
+
out: list[dict[str, Any]] = []
|
|
49
|
+
for m in messages:
|
|
50
|
+
role = (m.get("role") or "user").lower()
|
|
51
|
+
content = m.get("content")
|
|
52
|
+
parts: list[dict[str, Any]] = []
|
|
53
|
+
|
|
54
|
+
if isinstance(content, str):
|
|
55
|
+
parts.append({"type": "text", "text": content})
|
|
56
|
+
elif isinstance(content, list):
|
|
57
|
+
for p in content:
|
|
58
|
+
if not isinstance(p, dict):
|
|
59
|
+
continue
|
|
60
|
+
t = p.get("type")
|
|
61
|
+
|
|
62
|
+
if t in ("text", "input_text", "output_text"):
|
|
63
|
+
parts.append({"type": "text", "text": p.get("text", "")})
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
if t == "image_url":
|
|
67
|
+
iu = p.get("image_url") or {}
|
|
68
|
+
url = iu.get("url") or p.get("url")
|
|
69
|
+
if isinstance(url, str):
|
|
70
|
+
parts.append({"type": "image", "image": ImageInput(url=url)})
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
if t == "input_image":
|
|
74
|
+
url = p.get("image_url")
|
|
75
|
+
if isinstance(url, str):
|
|
76
|
+
parts.append({"type": "image", "image": ImageInput(url=url)})
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
if t == "image":
|
|
80
|
+
src = p.get("source") or {}
|
|
81
|
+
if src.get("type") == "base64":
|
|
82
|
+
parts.append(
|
|
83
|
+
{
|
|
84
|
+
"type": "image",
|
|
85
|
+
"image": ImageInput(
|
|
86
|
+
b64=src.get("data"), mime_type=src.get("media_type")
|
|
87
|
+
),
|
|
88
|
+
}
|
|
89
|
+
)
|
|
90
|
+
elif src.get("type") == "url":
|
|
91
|
+
parts.append({"type": "image", "image": ImageInput(url=src.get("url"))})
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
out.append({"role": role, "parts": parts})
|
|
95
|
+
return out
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _has_images(norm: Sequence[dict[str, Any]]) -> bool:
|
|
99
|
+
return any(p.get("type") == "image" for m in norm for p in m.get("parts", []))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _strip_code_fences(s: str) -> str:
|
|
103
|
+
s = s.strip()
|
|
104
|
+
if s.startswith("```"):
|
|
105
|
+
s = re.sub(r"^```[a-zA-Z0-9_-]*\s*", "", s)
|
|
106
|
+
s = re.sub(r"\s*```$", "", s)
|
|
107
|
+
return s.strip()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _extract_json_text(text: str) -> str:
|
|
111
|
+
t = _strip_code_fences(text)
|
|
112
|
+
if not t:
|
|
113
|
+
return t
|
|
114
|
+
if t[0] in "{[":
|
|
115
|
+
return t
|
|
116
|
+
m = re.search(r"(\{.*\}|\[.*\])", t, flags=re.DOTALL)
|
|
117
|
+
return m.group(1).strip() if m else t
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _validate_json_schema(obj: Any, schema: dict[str, Any]) -> None:
|
|
121
|
+
try:
|
|
122
|
+
import jsonschema # type: ignore
|
|
123
|
+
except Exception:
|
|
124
|
+
return
|
|
125
|
+
jsonschema.validate(instance=obj, schema=schema)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _ensure_system_json_directive(
|
|
129
|
+
messages: list[dict[str, Any]], *, schema: dict[str, Any] | None
|
|
130
|
+
) -> list[dict[str, Any]]:
|
|
131
|
+
directive = "Return ONLY valid JSON. No markdown, no commentary."
|
|
132
|
+
if schema is not None:
|
|
133
|
+
directive += "\nThe JSON MUST conform to this JSON Schema:\n" + json.dumps(
|
|
134
|
+
schema, ensure_ascii=False
|
|
135
|
+
)
|
|
136
|
+
return [{"role": "system", "content": directive}] + list(messages)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _message_content_has_images(messages: list[dict[str, Any]]) -> bool:
|
|
140
|
+
for m in messages:
|
|
141
|
+
c = m.get("content")
|
|
142
|
+
if isinstance(c, list):
|
|
143
|
+
for p in c:
|
|
144
|
+
if isinstance(p, dict) and p.get("type") in ("image_url", "input_image", "image"):
|
|
145
|
+
return True
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _to_anthropic_blocks(content: Any) -> list[dict[str, Any]]:
|
|
150
|
+
"""
|
|
151
|
+
Accept:
|
|
152
|
+
- str -> [{"type":"text","text":...}]
|
|
153
|
+
- OpenAI multimodal list -> convert data URLs -> anthropic base64 image blocks
|
|
154
|
+
- Anthropic blocks -> passthrough
|
|
155
|
+
"""
|
|
156
|
+
if isinstance(content, str):
|
|
157
|
+
return [{"type": "text", "text": content}]
|
|
158
|
+
|
|
159
|
+
if isinstance(content, list):
|
|
160
|
+
blocks: list[dict[str, Any]] = []
|
|
161
|
+
for p in content:
|
|
162
|
+
if not isinstance(p, dict):
|
|
163
|
+
continue
|
|
164
|
+
t = p.get("type")
|
|
165
|
+
if t in ("text", "input_text", "output_text"):
|
|
166
|
+
blocks.append({"type": "text", "text": p.get("text", "")})
|
|
167
|
+
elif t == "image" and "source" in p:
|
|
168
|
+
blocks.append(p)
|
|
169
|
+
elif t in ("image_url", "input_image"):
|
|
170
|
+
url = None
|
|
171
|
+
if t == "image_url":
|
|
172
|
+
url = (p.get("image_url") or {}).get("url") or p.get("url")
|
|
173
|
+
else:
|
|
174
|
+
url = p.get("image_url")
|
|
175
|
+
if isinstance(url, str) and _is_data_url(url):
|
|
176
|
+
b64, mime = _data_url_to_b64_and_mime(url)
|
|
177
|
+
blocks.append(
|
|
178
|
+
{
|
|
179
|
+
"type": "image",
|
|
180
|
+
"source": {"type": "base64", "media_type": mime, "data": b64},
|
|
181
|
+
}
|
|
182
|
+
)
|
|
183
|
+
elif isinstance(url, str):
|
|
184
|
+
raise RuntimeError(
|
|
185
|
+
"Anthropic vision: provide data: URLs (base64) for images (no remote fetch in client)."
|
|
186
|
+
)
|
|
187
|
+
return blocks
|
|
188
|
+
|
|
189
|
+
return [{"type": "text", "text": str(content)}]
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _to_gemini_parts(content: Any) -> list[dict[str, Any]]:
|
|
193
|
+
"""
|
|
194
|
+
Gemini REST generateContent supports:
|
|
195
|
+
- {"text": "..."}
|
|
196
|
+
- {"inline_data": {"mime_type": "...", "data": "<base64>"}}
|
|
197
|
+
"""
|
|
198
|
+
if isinstance(content, str):
|
|
199
|
+
return [{"text": content}]
|
|
200
|
+
|
|
201
|
+
if isinstance(content, list):
|
|
202
|
+
parts: list[dict[str, Any]] = []
|
|
203
|
+
for p in content:
|
|
204
|
+
if not isinstance(p, dict):
|
|
205
|
+
continue
|
|
206
|
+
t = p.get("type")
|
|
207
|
+
if t in ("text", "input_text", "output_text"):
|
|
208
|
+
parts.append({"text": p.get("text", "")})
|
|
209
|
+
elif t in ("image_url", "input_image"):
|
|
210
|
+
url = None
|
|
211
|
+
if t == "image_url":
|
|
212
|
+
url = (p.get("image_url") or {}).get("url") or p.get("url")
|
|
213
|
+
else:
|
|
214
|
+
url = p.get("image_url")
|
|
215
|
+
if isinstance(url, str) and _is_data_url(url):
|
|
216
|
+
b64, mime = _data_url_to_b64_and_mime(url)
|
|
217
|
+
parts.append({"inline_data": {"mime_type": mime, "data": b64}})
|
|
218
|
+
elif isinstance(url, str):
|
|
219
|
+
raise RuntimeError(
|
|
220
|
+
"Gemini vision: provide data: URLs (base64) or file_uri; remote http(s) URLs not accepted inline."
|
|
221
|
+
)
|
|
222
|
+
return parts
|
|
223
|
+
|
|
224
|
+
return [{"text": str(content)}]
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _normalize_openai_responses_input(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
228
|
+
"""
|
|
229
|
+
Make OpenAI Responses input robust:
|
|
230
|
+
- If content is str: keep as-is
|
|
231
|
+
- If content is OpenAI chat multimodal parts (text/image_url): convert to input_text/input_image
|
|
232
|
+
- If already input_text/input_image: passthrough
|
|
233
|
+
"""
|
|
234
|
+
out: list[dict[str, Any]] = []
|
|
235
|
+
for m in messages:
|
|
236
|
+
role = m.get("role", "user")
|
|
237
|
+
c = m.get("content")
|
|
238
|
+
|
|
239
|
+
if isinstance(c, str):
|
|
240
|
+
out.append({"role": role, "content": c})
|
|
241
|
+
continue
|
|
242
|
+
|
|
243
|
+
if isinstance(c, list):
|
|
244
|
+
blocks: list[dict[str, Any]] = []
|
|
245
|
+
for p in c:
|
|
246
|
+
if not isinstance(p, dict):
|
|
247
|
+
continue
|
|
248
|
+
t = p.get("type")
|
|
249
|
+
if t in ("input_text", "input_image"):
|
|
250
|
+
blocks.append(p)
|
|
251
|
+
elif t in ("text", "output_text"):
|
|
252
|
+
blocks.append({"type": "input_text", "text": p.get("text", "")})
|
|
253
|
+
elif t == "image_url":
|
|
254
|
+
url = (p.get("image_url") or {}).get("url") or p.get("url")
|
|
255
|
+
if isinstance(url, str):
|
|
256
|
+
blocks.append({"type": "input_image", "image_url": url})
|
|
257
|
+
else:
|
|
258
|
+
# ignore unknown part types
|
|
259
|
+
pass
|
|
260
|
+
out.append({"role": role, "content": blocks})
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
out.append({"role": role, "content": str(c)})
|
|
264
|
+
return out
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _normalize_base_url_no_trailing_slash(url: str) -> str:
|
|
268
|
+
return (url or "").strip().rstrip("/")
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _azure_images_generations_url(endpoint: str, deployment: str, api_version: str) -> str:
|
|
272
|
+
# endpoint example: https://<resource>.openai.azure.com
|
|
273
|
+
ep = _normalize_base_url_no_trailing_slash(endpoint)
|
|
274
|
+
return f"{ep}/openai/deployments/{deployment}/images/generations?api-version={api_version}"
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _guess_mime_from_format(fmt: str) -> str:
|
|
278
|
+
if fmt == "png":
|
|
279
|
+
return "image/png"
|
|
280
|
+
if fmt == "jpeg":
|
|
281
|
+
return "image/jpeg"
|
|
282
|
+
if fmt == "webp":
|
|
283
|
+
return "image/webp"
|
|
284
|
+
return "application/octet-stream"
|
|
@@ -123,6 +123,9 @@ class StdLoggerService(LoggerService):
|
|
|
123
123
|
def for_inspect(self) -> logging.Logger:
|
|
124
124
|
return self.for_namespace("inspect")
|
|
125
125
|
|
|
126
|
+
def for_channel(self) -> logging.Logger:
|
|
127
|
+
return self.for_namespace("channel")
|
|
128
|
+
|
|
126
129
|
def for_scheduler(self) -> logging.Logger:
|
|
127
130
|
return self.for_namespace("scheduler")
|
|
128
131
|
|
|
@@ -10,6 +10,44 @@ from aethergraph.contracts.services.mcp import MCPClientProtocol, MCPResource, M
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class HttpMCPClient(MCPClientProtocol):
|
|
13
|
+
"""
|
|
14
|
+
Initialize the HTTP client service with base URL, headers, and timeout.
|
|
15
|
+
|
|
16
|
+
This constructor sets up the base URL for all requests, applies default and custom headers,
|
|
17
|
+
and configures the request timeout. It also initializes internal state for the asynchronous
|
|
18
|
+
HTTP client and concurrency control.
|
|
19
|
+
|
|
20
|
+
Examples:
|
|
21
|
+
Basic usage with default headers:
|
|
22
|
+
```python
|
|
23
|
+
from aethergraph.services.mcp import HttpMCPClient
|
|
24
|
+
client = HttpMCPClient("https://api.example.com")
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Custom headers and timeout:
|
|
28
|
+
```python
|
|
29
|
+
from aethergraph.services.mcp import HttpMCPClient
|
|
30
|
+
client = HttpMCPClient(
|
|
31
|
+
"https://api.example.com",
|
|
32
|
+
headers={"Authorization": "Bearer <token>"},
|
|
33
|
+
timeout=30.0
|
|
34
|
+
)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
base_url: The root URL for all HTTP requests (e.g., "https://api.example.com").
|
|
39
|
+
headers: Optional dictionary of additional HTTP headers to include with each request.
|
|
40
|
+
The "Content-Type: application/json" header is always set by default.
|
|
41
|
+
timeout: The maximum time (in seconds) to wait for a response before timing out.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
None: Initializes the HttpMCPClient instance.
|
|
45
|
+
|
|
46
|
+
Notes:
|
|
47
|
+
- Ensure that the base_url does not have a trailing slash; it will be added automatically.
|
|
48
|
+
- The client uses asynchronous HTTP requests for non-blocking operations.
|
|
49
|
+
"""
|
|
50
|
+
|
|
13
51
|
def __init__(
|
|
14
52
|
self,
|
|
15
53
|
base_url: str,
|