spaik-sdk 0.6.2__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.
- spaik_sdk/__init__.py +21 -0
- spaik_sdk/agent/__init__.py +0 -0
- spaik_sdk/agent/base_agent.py +249 -0
- spaik_sdk/attachments/__init__.py +22 -0
- spaik_sdk/attachments/builder.py +61 -0
- spaik_sdk/attachments/file_storage_provider.py +27 -0
- spaik_sdk/attachments/mime_types.py +118 -0
- spaik_sdk/attachments/models.py +63 -0
- spaik_sdk/attachments/provider_support.py +53 -0
- spaik_sdk/attachments/storage/__init__.py +0 -0
- spaik_sdk/attachments/storage/base_file_storage.py +32 -0
- spaik_sdk/attachments/storage/impl/__init__.py +0 -0
- spaik_sdk/attachments/storage/impl/local_file_storage.py +101 -0
- spaik_sdk/audio/__init__.py +12 -0
- spaik_sdk/audio/options.py +53 -0
- spaik_sdk/audio/providers/__init__.py +1 -0
- spaik_sdk/audio/providers/google_tts.py +77 -0
- spaik_sdk/audio/providers/openai_stt.py +71 -0
- spaik_sdk/audio/providers/openai_tts.py +111 -0
- spaik_sdk/audio/stt.py +61 -0
- spaik_sdk/audio/tts.py +124 -0
- spaik_sdk/config/credentials_provider.py +10 -0
- spaik_sdk/config/env.py +59 -0
- spaik_sdk/config/env_credentials_provider.py +7 -0
- spaik_sdk/config/get_credentials_provider.py +14 -0
- spaik_sdk/image_gen/__init__.py +9 -0
- spaik_sdk/image_gen/image_generator.py +83 -0
- spaik_sdk/image_gen/options.py +24 -0
- spaik_sdk/image_gen/providers/__init__.py +0 -0
- spaik_sdk/image_gen/providers/google.py +75 -0
- spaik_sdk/image_gen/providers/openai.py +60 -0
- spaik_sdk/llm/__init__.py +0 -0
- spaik_sdk/llm/cancellation_handle.py +10 -0
- spaik_sdk/llm/consumption/__init__.py +0 -0
- spaik_sdk/llm/consumption/consumption_estimate.py +26 -0
- spaik_sdk/llm/consumption/consumption_estimate_builder.py +113 -0
- spaik_sdk/llm/consumption/consumption_extractor.py +59 -0
- spaik_sdk/llm/consumption/token_usage.py +31 -0
- spaik_sdk/llm/converters.py +146 -0
- spaik_sdk/llm/cost/__init__.py +1 -0
- spaik_sdk/llm/cost/builtin_cost_provider.py +83 -0
- spaik_sdk/llm/cost/cost_estimate.py +8 -0
- spaik_sdk/llm/cost/cost_provider.py +28 -0
- spaik_sdk/llm/extract_error_message.py +37 -0
- spaik_sdk/llm/langchain_loop_manager.py +270 -0
- spaik_sdk/llm/langchain_service.py +196 -0
- spaik_sdk/llm/message_handler.py +188 -0
- spaik_sdk/llm/streaming/__init__.py +1 -0
- spaik_sdk/llm/streaming/block_manager.py +152 -0
- spaik_sdk/llm/streaming/models.py +42 -0
- spaik_sdk/llm/streaming/streaming_content_handler.py +157 -0
- spaik_sdk/llm/streaming/streaming_event_handler.py +215 -0
- spaik_sdk/llm/streaming/streaming_state_manager.py +58 -0
- spaik_sdk/models/__init__.py +0 -0
- spaik_sdk/models/factories/__init__.py +0 -0
- spaik_sdk/models/factories/anthropic_factory.py +33 -0
- spaik_sdk/models/factories/base_model_factory.py +71 -0
- spaik_sdk/models/factories/google_factory.py +30 -0
- spaik_sdk/models/factories/ollama_factory.py +41 -0
- spaik_sdk/models/factories/openai_factory.py +50 -0
- spaik_sdk/models/llm_config.py +46 -0
- spaik_sdk/models/llm_families.py +7 -0
- spaik_sdk/models/llm_model.py +17 -0
- spaik_sdk/models/llm_wrapper.py +25 -0
- spaik_sdk/models/model_registry.py +156 -0
- spaik_sdk/models/providers/__init__.py +0 -0
- spaik_sdk/models/providers/anthropic_provider.py +29 -0
- spaik_sdk/models/providers/azure_provider.py +31 -0
- spaik_sdk/models/providers/base_provider.py +62 -0
- spaik_sdk/models/providers/google_provider.py +26 -0
- spaik_sdk/models/providers/ollama_provider.py +26 -0
- spaik_sdk/models/providers/openai_provider.py +26 -0
- spaik_sdk/models/providers/provider_type.py +90 -0
- spaik_sdk/orchestration/__init__.py +24 -0
- spaik_sdk/orchestration/base_orchestrator.py +238 -0
- spaik_sdk/orchestration/checkpoint.py +80 -0
- spaik_sdk/orchestration/models.py +103 -0
- spaik_sdk/prompt/__init__.py +0 -0
- spaik_sdk/prompt/get_prompt_loader.py +13 -0
- spaik_sdk/prompt/local_prompt_loader.py +21 -0
- spaik_sdk/prompt/prompt_loader.py +48 -0
- spaik_sdk/prompt/prompt_loader_mode.py +14 -0
- spaik_sdk/py.typed +1 -0
- spaik_sdk/recording/__init__.py +1 -0
- spaik_sdk/recording/base_playback.py +90 -0
- spaik_sdk/recording/base_recorder.py +50 -0
- spaik_sdk/recording/conditional_recorder.py +38 -0
- spaik_sdk/recording/impl/__init__.py +1 -0
- spaik_sdk/recording/impl/local_playback.py +76 -0
- spaik_sdk/recording/impl/local_recorder.py +85 -0
- spaik_sdk/recording/langchain_serializer.py +88 -0
- spaik_sdk/server/__init__.py +1 -0
- spaik_sdk/server/api/routers/__init__.py +0 -0
- spaik_sdk/server/api/routers/api_builder.py +149 -0
- spaik_sdk/server/api/routers/audio_router_factory.py +201 -0
- spaik_sdk/server/api/routers/file_router_factory.py +111 -0
- spaik_sdk/server/api/routers/thread_router_factory.py +284 -0
- spaik_sdk/server/api/streaming/__init__.py +0 -0
- spaik_sdk/server/api/streaming/format_sse_event.py +41 -0
- spaik_sdk/server/api/streaming/negotiate_streaming_response.py +8 -0
- spaik_sdk/server/api/streaming/streaming_negotiator.py +10 -0
- spaik_sdk/server/authorization/__init__.py +0 -0
- spaik_sdk/server/authorization/base_authorizer.py +64 -0
- spaik_sdk/server/authorization/base_user.py +13 -0
- spaik_sdk/server/authorization/dummy_authorizer.py +17 -0
- spaik_sdk/server/job_processor/__init__.py +0 -0
- spaik_sdk/server/job_processor/base_job_processor.py +8 -0
- spaik_sdk/server/job_processor/thread_job_processor.py +32 -0
- spaik_sdk/server/pubsub/__init__.py +1 -0
- spaik_sdk/server/pubsub/cancellation_publisher.py +7 -0
- spaik_sdk/server/pubsub/cancellation_subscriber.py +38 -0
- spaik_sdk/server/pubsub/event_publisher.py +13 -0
- spaik_sdk/server/pubsub/impl/__init__.py +1 -0
- spaik_sdk/server/pubsub/impl/local_cancellation_pubsub.py +48 -0
- spaik_sdk/server/pubsub/impl/signalr_publisher.py +36 -0
- spaik_sdk/server/queue/__init__.py +1 -0
- spaik_sdk/server/queue/agent_job_queue.py +27 -0
- spaik_sdk/server/queue/impl/__init__.py +1 -0
- spaik_sdk/server/queue/impl/azure_queue.py +24 -0
- spaik_sdk/server/response/__init__.py +0 -0
- spaik_sdk/server/response/agent_response_generator.py +39 -0
- spaik_sdk/server/response/response_generator.py +13 -0
- spaik_sdk/server/response/simple_agent_response_generator.py +14 -0
- spaik_sdk/server/services/__init__.py +0 -0
- spaik_sdk/server/services/thread_converters.py +113 -0
- spaik_sdk/server/services/thread_models.py +90 -0
- spaik_sdk/server/services/thread_service.py +91 -0
- spaik_sdk/server/storage/__init__.py +1 -0
- spaik_sdk/server/storage/base_thread_repository.py +51 -0
- spaik_sdk/server/storage/impl/__init__.py +0 -0
- spaik_sdk/server/storage/impl/in_memory_thread_repository.py +100 -0
- spaik_sdk/server/storage/impl/local_file_thread_repository.py +217 -0
- spaik_sdk/server/storage/thread_filter.py +166 -0
- spaik_sdk/server/storage/thread_metadata.py +53 -0
- spaik_sdk/thread/__init__.py +0 -0
- spaik_sdk/thread/adapters/__init__.py +0 -0
- spaik_sdk/thread/adapters/cli/__init__.py +0 -0
- spaik_sdk/thread/adapters/cli/block_display.py +92 -0
- spaik_sdk/thread/adapters/cli/display_manager.py +84 -0
- spaik_sdk/thread/adapters/cli/live_cli.py +235 -0
- spaik_sdk/thread/adapters/event_adapter.py +28 -0
- spaik_sdk/thread/adapters/streaming_block_adapter.py +57 -0
- spaik_sdk/thread/adapters/sync_adapter.py +76 -0
- spaik_sdk/thread/models.py +224 -0
- spaik_sdk/thread/thread_container.py +468 -0
- spaik_sdk/tools/__init__.py +0 -0
- spaik_sdk/tools/impl/__init__.py +0 -0
- spaik_sdk/tools/impl/mcp_tool_provider.py +93 -0
- spaik_sdk/tools/impl/search_tool_provider.py +18 -0
- spaik_sdk/tools/tool_provider.py +131 -0
- spaik_sdk/tracing/__init__.py +13 -0
- spaik_sdk/tracing/agent_trace.py +72 -0
- spaik_sdk/tracing/get_trace_sink.py +15 -0
- spaik_sdk/tracing/local_trace_sink.py +23 -0
- spaik_sdk/tracing/trace_sink.py +19 -0
- spaik_sdk/tracing/trace_sink_mode.py +14 -0
- spaik_sdk/utils/__init__.py +0 -0
- spaik_sdk/utils/init_logger.py +24 -0
- spaik_sdk-0.6.2.dist-info/METADATA +379 -0
- spaik_sdk-0.6.2.dist-info/RECORD +161 -0
- spaik_sdk-0.6.2.dist-info/WHEEL +4 -0
spaik_sdk/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Spaik SDK - SDK for building various kinds of agentic + AI solutions."""
|
|
2
|
+
|
|
3
|
+
from .agent.base_agent import BaseAgent
|
|
4
|
+
from .models.llm_config import LLMConfig
|
|
5
|
+
from .models.llm_model import LLMModel
|
|
6
|
+
from .models.providers.provider_type import ProviderType
|
|
7
|
+
from .thread.models import MessageBlock, MessageBlockType, ThreadMessage
|
|
8
|
+
from .thread.thread_container import ThreadContainer
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"BaseAgent",
|
|
12
|
+
"LLMConfig",
|
|
13
|
+
"LLMModel",
|
|
14
|
+
"ProviderType",
|
|
15
|
+
"ThreadContainer",
|
|
16
|
+
"ThreadMessage",
|
|
17
|
+
"MessageBlock",
|
|
18
|
+
"MessageBlockType",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
__version__ = "0.0.1"
|
|
File without changes
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from abc import ABC
|
|
3
|
+
from typing import Any, AsyncGenerator, Dict, List, Optional, Type, TypeVar
|
|
4
|
+
|
|
5
|
+
from langchain_core.language_models import BaseChatModel
|
|
6
|
+
from langchain_core.tools import BaseTool
|
|
7
|
+
from langgraph.graph.state import CompiledStateGraph
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from spaik_sdk.attachments.models import Attachment
|
|
11
|
+
from spaik_sdk.config.env import env_config
|
|
12
|
+
from spaik_sdk.llm.cancellation_handle import CancellationHandle
|
|
13
|
+
from spaik_sdk.llm.cost.builtin_cost_provider import BuiltinCostProvider
|
|
14
|
+
from spaik_sdk.llm.cost.cost_estimate import CostEstimate
|
|
15
|
+
from spaik_sdk.llm.cost.cost_provider import CostProvider
|
|
16
|
+
from spaik_sdk.llm.langchain_service import LangChainService
|
|
17
|
+
from spaik_sdk.models.llm_config import LLMConfig
|
|
18
|
+
from spaik_sdk.models.llm_model import LLMModel
|
|
19
|
+
from spaik_sdk.models.providers.provider_type import ProviderType
|
|
20
|
+
from spaik_sdk.prompt.get_prompt_loader import get_prompt_loader
|
|
21
|
+
from spaik_sdk.prompt.prompt_loader import PromptLoader
|
|
22
|
+
from spaik_sdk.prompt.prompt_loader_mode import PromptLoaderMode
|
|
23
|
+
from spaik_sdk.recording.conditional_recorder import ConditionalRecorder
|
|
24
|
+
from spaik_sdk.thread.adapters.cli.live_cli import LiveCLI
|
|
25
|
+
from spaik_sdk.thread.adapters.event_adapter import EventAdapter
|
|
26
|
+
from spaik_sdk.thread.adapters.sync_adapter import SyncAdapter
|
|
27
|
+
from spaik_sdk.thread.models import BlockFullyAddedEvent, ThreadEvent, ThreadMessage
|
|
28
|
+
from spaik_sdk.thread.thread_container import ThreadContainer
|
|
29
|
+
from spaik_sdk.tools.tool_provider import ToolProvider
|
|
30
|
+
from spaik_sdk.tracing.agent_trace import AgentTrace
|
|
31
|
+
from spaik_sdk.tracing.trace_sink import TraceSink
|
|
32
|
+
from spaik_sdk.utils.init_logger import init_logger
|
|
33
|
+
|
|
34
|
+
logger = init_logger(__name__)
|
|
35
|
+
|
|
36
|
+
T = TypeVar("T", bound=BaseModel)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class BaseAgent(ABC):
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
system_prompt_args: Dict[str, Any] = {},
|
|
43
|
+
system_prompt_version: Optional[str] = None,
|
|
44
|
+
system_prompt: Optional[str] = None,
|
|
45
|
+
prompt_loader: Optional[PromptLoader] = None,
|
|
46
|
+
prompt_loader_mode: Optional[PromptLoaderMode] = None,
|
|
47
|
+
llm_config: Optional[LLMConfig] = None,
|
|
48
|
+
llm_model: Optional[LLMModel] = None,
|
|
49
|
+
reasoning: Optional[bool] = None,
|
|
50
|
+
trace: Optional[AgentTrace] = None,
|
|
51
|
+
trace_sink: Optional[TraceSink] = None,
|
|
52
|
+
thread_container: Optional[ThreadContainer] = None,
|
|
53
|
+
tools: Optional[List[BaseTool]] = None,
|
|
54
|
+
tool_providers: Optional[List[ToolProvider]] = None,
|
|
55
|
+
recorder: Optional[ConditionalRecorder] = None,
|
|
56
|
+
cancellation_handle: Optional[CancellationHandle] = None,
|
|
57
|
+
cost_provider: Optional[CostProvider] = None,
|
|
58
|
+
):
|
|
59
|
+
logger.debug("Initializing BaseAgent")
|
|
60
|
+
self.prompt_loader = prompt_loader or get_prompt_loader(prompt_loader_mode)
|
|
61
|
+
self.system_prompt = system_prompt or self._get_system_prompt(system_prompt_args, system_prompt_version)
|
|
62
|
+
self.trace = trace or AgentTrace(self.system_prompt, self.__class__.__name__, trace_sink=trace_sink)
|
|
63
|
+
self.thread_container = thread_container or ThreadContainer(self.system_prompt)
|
|
64
|
+
self.tools = tools or self._create_tools(tool_providers)
|
|
65
|
+
self.llm_config = llm_config or self.create_llm_config(llm_model, reasoning)
|
|
66
|
+
self.recorder = recorder.get_recorder() if recorder is not None else None
|
|
67
|
+
self.playback = recorder.get_playback() if recorder is not None else None
|
|
68
|
+
self.thread_container.subscribe(self._on_thread_event)
|
|
69
|
+
self.cancellation_handle = cancellation_handle
|
|
70
|
+
self.cost_provider = cost_provider or BuiltinCostProvider()
|
|
71
|
+
|
|
72
|
+
def get_response_stream(
|
|
73
|
+
self,
|
|
74
|
+
user_input: Optional[str] = None,
|
|
75
|
+
attachments: Optional[List[Attachment]] = None,
|
|
76
|
+
) -> AsyncGenerator[Dict[str, Any], None]:
|
|
77
|
+
self.trace.add_input(user_input)
|
|
78
|
+
langchain_service = self._create_langchain_service()
|
|
79
|
+
return langchain_service.execute_stream_tokens(user_input, self.tools, attachments)
|
|
80
|
+
|
|
81
|
+
async def get_event_stream(
|
|
82
|
+
self,
|
|
83
|
+
user_input: Optional[str] = None,
|
|
84
|
+
attachments: Optional[List[Attachment]] = None,
|
|
85
|
+
) -> AsyncGenerator[ThreadEvent, None]:
|
|
86
|
+
event_adapter = EventAdapter(self.thread_container)
|
|
87
|
+
async for _event in self.get_response_stream(user_input, attachments):
|
|
88
|
+
new_events = event_adapter.flush()
|
|
89
|
+
for new_event in new_events:
|
|
90
|
+
yield new_event
|
|
91
|
+
event_adapter.cleanup()
|
|
92
|
+
|
|
93
|
+
def get_response(
|
|
94
|
+
self,
|
|
95
|
+
user_input: Optional[str] = None,
|
|
96
|
+
attachments: Optional[List[Attachment]] = None,
|
|
97
|
+
) -> ThreadMessage:
|
|
98
|
+
return asyncio.run(self.get_response_async(user_input, attachments))
|
|
99
|
+
|
|
100
|
+
async def get_response_async(
|
|
101
|
+
self,
|
|
102
|
+
user_input: Optional[str] = None,
|
|
103
|
+
attachments: Optional[List[Attachment]] = None,
|
|
104
|
+
) -> ThreadMessage:
|
|
105
|
+
sync_adapter = SyncAdapter(self.thread_container)
|
|
106
|
+
await sync_adapter.run_async(self.get_response_stream(user_input, attachments))
|
|
107
|
+
ret = await sync_adapter.wait_for_completion_async()
|
|
108
|
+
if ret is None:
|
|
109
|
+
raise ValueError("No response received")
|
|
110
|
+
self.thread_container.complete_generation()
|
|
111
|
+
return ret
|
|
112
|
+
|
|
113
|
+
def get_response_text(
|
|
114
|
+
self,
|
|
115
|
+
user_input: Optional[str] = None,
|
|
116
|
+
attachments: Optional[List[Attachment]] = None,
|
|
117
|
+
) -> str:
|
|
118
|
+
return self.get_response(user_input, attachments).get_text_content()
|
|
119
|
+
|
|
120
|
+
async def get_response_text_async(
|
|
121
|
+
self,
|
|
122
|
+
user_input: Optional[str] = None,
|
|
123
|
+
attachments: Optional[List[Attachment]] = None,
|
|
124
|
+
) -> str:
|
|
125
|
+
return (await self.get_response_async(user_input, attachments)).get_text_content()
|
|
126
|
+
|
|
127
|
+
def get_structured_response(self, prompt: str, output_schema: Type[T]) -> T:
|
|
128
|
+
self.trace.add_structured_response_input(prompt, output_schema)
|
|
129
|
+
llm_config = self.llm_config.as_structured_response_config()
|
|
130
|
+
langchain_service = self._create_langchain_service(llm_config)
|
|
131
|
+
ret = langchain_service.get_structured_response(prompt, output_schema)
|
|
132
|
+
self.trace.add_structured_response_output(ret)
|
|
133
|
+
return ret
|
|
134
|
+
|
|
135
|
+
def run_cli(self):
|
|
136
|
+
asyncio.run(LiveCLI(self.thread_container).run_interactive(self))
|
|
137
|
+
|
|
138
|
+
def create_llm_config(self, llm_model: Optional[LLMModel] = None, reasoning: Optional[bool] = None) -> LLMConfig:
|
|
139
|
+
if llm_model is None:
|
|
140
|
+
llm_model = self.get_llm_model()
|
|
141
|
+
|
|
142
|
+
provider_type = ProviderType.from_family(llm_model.family)
|
|
143
|
+
|
|
144
|
+
return LLMConfig(
|
|
145
|
+
model=llm_model,
|
|
146
|
+
provider_type=provider_type,
|
|
147
|
+
reasoning=reasoning if reasoning is not None else llm_model.reasoning,
|
|
148
|
+
tool_usage=len(self.tools) > 0,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
def get_llm_model(self) -> LLMModel:
|
|
152
|
+
return env_config.get_default_model()
|
|
153
|
+
|
|
154
|
+
def is_reasoning(self) -> bool:
|
|
155
|
+
return True
|
|
156
|
+
|
|
157
|
+
def get_tool_providers(self) -> List[ToolProvider]:
|
|
158
|
+
return []
|
|
159
|
+
|
|
160
|
+
def set_thread_container(self, thread_container: ThreadContainer) -> None:
|
|
161
|
+
self.thread_container = thread_container
|
|
162
|
+
if self.thread_container.system_prompt is None:
|
|
163
|
+
self.thread_container.system_prompt = self.system_prompt
|
|
164
|
+
self.thread_container.subscribe(self._on_thread_event)
|
|
165
|
+
|
|
166
|
+
def set_cancellation_handle(self, cancellation_handle: Optional[CancellationHandle]) -> None:
|
|
167
|
+
self.cancellation_handle = cancellation_handle
|
|
168
|
+
|
|
169
|
+
def _get_prompt(self, prompt_name: str, args: Dict[str, Any], version: Optional[str] = None) -> str:
|
|
170
|
+
return self.prompt_loader.get_agent_prompt(self.__class__, prompt_name, args, version)
|
|
171
|
+
|
|
172
|
+
def _get_system_prompt(self, args: Dict[str, Any], version: Optional[str] = None) -> str:
|
|
173
|
+
return self.prompt_loader.get_system_prompt(self.__class__, args, version)
|
|
174
|
+
|
|
175
|
+
def _create_tools(self, tool_providers: Optional[List[ToolProvider]] = None) -> List[BaseTool]:
|
|
176
|
+
tool_providers = tool_providers or self.get_tool_providers()
|
|
177
|
+
tools = []
|
|
178
|
+
for provider in tool_providers:
|
|
179
|
+
tools.extend(provider.get_tools())
|
|
180
|
+
return tools
|
|
181
|
+
|
|
182
|
+
def _create_langchain_service(self, llm_config: Optional[LLMConfig] = None) -> LangChainService:
|
|
183
|
+
if self.thread_container is None:
|
|
184
|
+
raise ValueError("Thread container is not set")
|
|
185
|
+
return LangChainService(
|
|
186
|
+
llm_config or self.llm_config,
|
|
187
|
+
self.thread_container,
|
|
188
|
+
self.__class__.__name__,
|
|
189
|
+
self.__class__.__name__,
|
|
190
|
+
self.recorder,
|
|
191
|
+
self.playback,
|
|
192
|
+
self.cancellation_handle,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def _on_thread_event(self, event: ThreadEvent) -> None:
|
|
196
|
+
logger.debug(f"Thread event: {event}")
|
|
197
|
+
"""Handle thread events and forward to trace"""
|
|
198
|
+
if isinstance(event, BlockFullyAddedEvent):
|
|
199
|
+
self.trace.add_block(event.block)
|
|
200
|
+
|
|
201
|
+
def get_cost(self, latest_only: bool = False) -> CostEstimate:
|
|
202
|
+
token_usage = self.thread_container.get_token_usage()
|
|
203
|
+
if latest_only:
|
|
204
|
+
token_usage = self.thread_container.get_latest_token_usage()
|
|
205
|
+
if token_usage is None:
|
|
206
|
+
from spaik_sdk.llm.consumption.token_usage import TokenUsage
|
|
207
|
+
|
|
208
|
+
token_usage = TokenUsage(input_tokens=0, output_tokens=0)
|
|
209
|
+
return self.cost_provider.get_cost_estimate(self.get_llm_model(), token_usage)
|
|
210
|
+
|
|
211
|
+
def get_langchain_model(self) -> BaseChatModel:
|
|
212
|
+
"""Get the underlying LangChain chat model.
|
|
213
|
+
|
|
214
|
+
Returns the configured LangChain BaseChatModel that can be used directly
|
|
215
|
+
with LangChain or LangGraph for custom workflows.
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
BaseChatModel: The LangChain chat model instance.
|
|
219
|
+
"""
|
|
220
|
+
return self.llm_config.get_model_wrapper().get_langchain_model()
|
|
221
|
+
|
|
222
|
+
def get_react_agent(self) -> CompiledStateGraph:
|
|
223
|
+
"""Get a LangGraph react agent with the agent's tools and model.
|
|
224
|
+
|
|
225
|
+
Returns a compiled LangGraph react agent that can be used directly
|
|
226
|
+
with LangGraph workflows, custom orchestration, or integrated into
|
|
227
|
+
larger agent systems.
|
|
228
|
+
|
|
229
|
+
The react agent includes all tools configured on this agent and uses
|
|
230
|
+
the agent's LLM configuration.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
CompiledStateGraph: A compiled LangGraph react agent.
|
|
234
|
+
|
|
235
|
+
Example:
|
|
236
|
+
```python
|
|
237
|
+
agent = MyAgent()
|
|
238
|
+
react_agent = agent.get_react_agent()
|
|
239
|
+
|
|
240
|
+
# Use directly with LangGraph
|
|
241
|
+
result = await react_agent.ainvoke({"messages": messages})
|
|
242
|
+
|
|
243
|
+
# Or stream events
|
|
244
|
+
async for event in react_agent.astream_events({"messages": messages}, version="v2"):
|
|
245
|
+
print(event)
|
|
246
|
+
```
|
|
247
|
+
"""
|
|
248
|
+
langchain_service = self._create_langchain_service()
|
|
249
|
+
return langchain_service.create_executor(self.tools)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from spaik_sdk.attachments.builder import AttachmentBuilder
|
|
2
|
+
from spaik_sdk.attachments.file_storage_provider import (
|
|
3
|
+
get_file_storage,
|
|
4
|
+
get_or_create_file_storage,
|
|
5
|
+
reset_file_storage,
|
|
6
|
+
set_file_storage,
|
|
7
|
+
)
|
|
8
|
+
from spaik_sdk.attachments.models import Attachment, FileMetadata
|
|
9
|
+
from spaik_sdk.attachments.storage.base_file_storage import BaseFileStorage
|
|
10
|
+
from spaik_sdk.attachments.storage.impl.local_file_storage import LocalFileStorage
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"Attachment",
|
|
14
|
+
"AttachmentBuilder",
|
|
15
|
+
"BaseFileStorage",
|
|
16
|
+
"FileMetadata",
|
|
17
|
+
"LocalFileStorage",
|
|
18
|
+
"get_file_storage",
|
|
19
|
+
"get_or_create_file_storage",
|
|
20
|
+
"reset_file_storage",
|
|
21
|
+
"set_file_storage",
|
|
22
|
+
]
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import List, Optional, Union
|
|
3
|
+
|
|
4
|
+
from spaik_sdk.attachments.mime_types import guess_mime_type
|
|
5
|
+
from spaik_sdk.attachments.models import Attachment
|
|
6
|
+
from spaik_sdk.attachments.storage.base_file_storage import BaseFileStorage
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AttachmentBuilder:
|
|
10
|
+
def __init__(self, file_storage: BaseFileStorage):
|
|
11
|
+
self.file_storage = file_storage
|
|
12
|
+
|
|
13
|
+
async def from_path(
|
|
14
|
+
self,
|
|
15
|
+
path: Union[str, Path],
|
|
16
|
+
owner_id: str = "system",
|
|
17
|
+
mime_type: Optional[str] = None,
|
|
18
|
+
) -> Attachment:
|
|
19
|
+
path = Path(path)
|
|
20
|
+
|
|
21
|
+
if not path.exists():
|
|
22
|
+
raise FileNotFoundError(f"File not found: {path}")
|
|
23
|
+
|
|
24
|
+
if mime_type is None:
|
|
25
|
+
mime_type = guess_mime_type(path)
|
|
26
|
+
if mime_type is None:
|
|
27
|
+
raise ValueError(f"Could not determine MIME type for: {path}")
|
|
28
|
+
|
|
29
|
+
with open(path, "rb") as f:
|
|
30
|
+
data = f.read()
|
|
31
|
+
|
|
32
|
+
metadata = await self.file_storage.store(
|
|
33
|
+
data=data,
|
|
34
|
+
mime_type=mime_type,
|
|
35
|
+
owner_id=owner_id,
|
|
36
|
+
filename=path.name,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
return metadata.to_attachment()
|
|
40
|
+
|
|
41
|
+
async def from_paths(
|
|
42
|
+
self,
|
|
43
|
+
paths: List[Union[str, Path]],
|
|
44
|
+
owner_id: str = "system",
|
|
45
|
+
) -> List[Attachment]:
|
|
46
|
+
return [await self.from_path(p, owner_id=owner_id) for p in paths]
|
|
47
|
+
|
|
48
|
+
async def from_bytes(
|
|
49
|
+
self,
|
|
50
|
+
data: bytes,
|
|
51
|
+
mime_type: str,
|
|
52
|
+
owner_id: str = "system",
|
|
53
|
+
filename: Optional[str] = None,
|
|
54
|
+
) -> Attachment:
|
|
55
|
+
metadata = await self.file_storage.store(
|
|
56
|
+
data=data,
|
|
57
|
+
mime_type=mime_type,
|
|
58
|
+
owner_id=owner_id,
|
|
59
|
+
filename=filename,
|
|
60
|
+
)
|
|
61
|
+
return metadata.to_attachment()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from spaik_sdk.attachments.storage.base_file_storage import BaseFileStorage
|
|
4
|
+
from spaik_sdk.attachments.storage.impl.local_file_storage import LocalFileStorage
|
|
5
|
+
|
|
6
|
+
_file_storage: Optional[BaseFileStorage] = None
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_file_storage() -> Optional[BaseFileStorage]:
|
|
10
|
+
return _file_storage
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def set_file_storage(storage: BaseFileStorage) -> None:
|
|
14
|
+
global _file_storage
|
|
15
|
+
_file_storage = storage
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_or_create_file_storage() -> BaseFileStorage:
|
|
19
|
+
global _file_storage
|
|
20
|
+
if _file_storage is None:
|
|
21
|
+
_file_storage = LocalFileStorage()
|
|
22
|
+
return _file_storage
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def reset_file_storage() -> None:
|
|
26
|
+
global _file_storage
|
|
27
|
+
_file_storage = None
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class MimeTypes:
|
|
6
|
+
# Images
|
|
7
|
+
PNG = "image/png"
|
|
8
|
+
JPEG = "image/jpeg"
|
|
9
|
+
GIF = "image/gif"
|
|
10
|
+
WEBP = "image/webp"
|
|
11
|
+
SVG = "image/svg+xml"
|
|
12
|
+
|
|
13
|
+
# Documents
|
|
14
|
+
PDF = "application/pdf"
|
|
15
|
+
|
|
16
|
+
# Audio
|
|
17
|
+
MP3 = "audio/mpeg"
|
|
18
|
+
WAV = "audio/wav"
|
|
19
|
+
OGG = "audio/ogg"
|
|
20
|
+
WEBM_AUDIO = "audio/webm"
|
|
21
|
+
|
|
22
|
+
# Video
|
|
23
|
+
MP4 = "video/mp4"
|
|
24
|
+
WEBM_VIDEO = "video/webm"
|
|
25
|
+
MOV = "video/quicktime"
|
|
26
|
+
|
|
27
|
+
# Text
|
|
28
|
+
PLAIN = "text/plain"
|
|
29
|
+
HTML = "text/html"
|
|
30
|
+
CSV = "text/csv"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
IMAGE_TYPES = frozenset(
|
|
34
|
+
{
|
|
35
|
+
MimeTypes.PNG,
|
|
36
|
+
MimeTypes.JPEG,
|
|
37
|
+
MimeTypes.GIF,
|
|
38
|
+
MimeTypes.WEBP,
|
|
39
|
+
MimeTypes.SVG,
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
DOCUMENT_TYPES = frozenset(
|
|
44
|
+
{
|
|
45
|
+
MimeTypes.PDF,
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
AUDIO_TYPES = frozenset(
|
|
50
|
+
{
|
|
51
|
+
MimeTypes.MP3,
|
|
52
|
+
MimeTypes.WAV,
|
|
53
|
+
MimeTypes.OGG,
|
|
54
|
+
MimeTypes.WEBM_AUDIO,
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
VIDEO_TYPES = frozenset(
|
|
59
|
+
{
|
|
60
|
+
MimeTypes.MP4,
|
|
61
|
+
MimeTypes.WEBM_VIDEO,
|
|
62
|
+
MimeTypes.MOV,
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
TEXT_TYPES = frozenset(
|
|
67
|
+
{
|
|
68
|
+
MimeTypes.PLAIN,
|
|
69
|
+
MimeTypes.HTML,
|
|
70
|
+
MimeTypes.CSV,
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
ALL_SUPPORTED_TYPES = IMAGE_TYPES | DOCUMENT_TYPES | AUDIO_TYPES | VIDEO_TYPES | TEXT_TYPES
|
|
75
|
+
|
|
76
|
+
EXTENSION_TO_MIME: dict[str, str] = {
|
|
77
|
+
".png": MimeTypes.PNG,
|
|
78
|
+
".jpg": MimeTypes.JPEG,
|
|
79
|
+
".jpeg": MimeTypes.JPEG,
|
|
80
|
+
".gif": MimeTypes.GIF,
|
|
81
|
+
".webp": MimeTypes.WEBP,
|
|
82
|
+
".svg": MimeTypes.SVG,
|
|
83
|
+
".pdf": MimeTypes.PDF,
|
|
84
|
+
".mp3": MimeTypes.MP3,
|
|
85
|
+
".wav": MimeTypes.WAV,
|
|
86
|
+
".ogg": MimeTypes.OGG,
|
|
87
|
+
".mp4": MimeTypes.MP4,
|
|
88
|
+
".webm": MimeTypes.WEBM_VIDEO,
|
|
89
|
+
".mov": MimeTypes.MOV,
|
|
90
|
+
".txt": MimeTypes.PLAIN,
|
|
91
|
+
".html": MimeTypes.HTML,
|
|
92
|
+
".csv": MimeTypes.CSV,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def guess_mime_type(path: str | Path) -> Optional[str]:
|
|
97
|
+
suffix = Path(path).suffix.lower()
|
|
98
|
+
return EXTENSION_TO_MIME.get(suffix)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def is_image(mime_type: str) -> bool:
|
|
102
|
+
return mime_type in IMAGE_TYPES
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def is_document(mime_type: str) -> bool:
|
|
106
|
+
return mime_type in DOCUMENT_TYPES
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def is_audio(mime_type: str) -> bool:
|
|
110
|
+
return mime_type in AUDIO_TYPES
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def is_video(mime_type: str) -> bool:
|
|
114
|
+
return mime_type in VIDEO_TYPES
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def is_supported(mime_type: str) -> bool:
|
|
118
|
+
return mime_type in ALL_SUPPORTED_TYPES
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class Attachment:
|
|
8
|
+
file_id: str
|
|
9
|
+
mime_type: str
|
|
10
|
+
filename: Optional[str] = None
|
|
11
|
+
|
|
12
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
13
|
+
return {
|
|
14
|
+
"file_id": self.file_id,
|
|
15
|
+
"mime_type": self.mime_type,
|
|
16
|
+
"filename": self.filename,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Attachment":
|
|
21
|
+
return cls(
|
|
22
|
+
file_id=data["file_id"],
|
|
23
|
+
mime_type=data["mime_type"],
|
|
24
|
+
filename=data.get("filename"),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class FileMetadata:
|
|
30
|
+
file_id: str
|
|
31
|
+
mime_type: str
|
|
32
|
+
owner_id: str
|
|
33
|
+
size_bytes: int
|
|
34
|
+
created_at: int = field(default_factory=lambda: int(time.time() * 1000))
|
|
35
|
+
filename: Optional[str] = None
|
|
36
|
+
|
|
37
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
38
|
+
return {
|
|
39
|
+
"file_id": self.file_id,
|
|
40
|
+
"mime_type": self.mime_type,
|
|
41
|
+
"filename": self.filename,
|
|
42
|
+
"owner_id": self.owner_id,
|
|
43
|
+
"created_at": self.created_at,
|
|
44
|
+
"size_bytes": self.size_bytes,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def from_dict(cls, data: Dict[str, Any]) -> "FileMetadata":
|
|
49
|
+
return cls(
|
|
50
|
+
file_id=data["file_id"],
|
|
51
|
+
mime_type=data["mime_type"],
|
|
52
|
+
filename=data.get("filename"),
|
|
53
|
+
owner_id=data["owner_id"],
|
|
54
|
+
created_at=data["created_at"],
|
|
55
|
+
size_bytes=data["size_bytes"],
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def to_attachment(self) -> Attachment:
|
|
59
|
+
return Attachment(
|
|
60
|
+
file_id=self.file_id,
|
|
61
|
+
mime_type=self.mime_type,
|
|
62
|
+
filename=self.filename,
|
|
63
|
+
)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from spaik_sdk.attachments.mime_types import (
|
|
2
|
+
AUDIO_TYPES,
|
|
3
|
+
DOCUMENT_TYPES,
|
|
4
|
+
IMAGE_TYPES,
|
|
5
|
+
VIDEO_TYPES,
|
|
6
|
+
MimeTypes,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
OPENAI_SUPPORTED = frozenset(
|
|
10
|
+
{
|
|
11
|
+
MimeTypes.PNG,
|
|
12
|
+
MimeTypes.JPEG,
|
|
13
|
+
MimeTypes.GIF,
|
|
14
|
+
MimeTypes.WEBP,
|
|
15
|
+
MimeTypes.PDF,
|
|
16
|
+
}
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
ANTHROPIC_SUPPORTED = frozenset(
|
|
20
|
+
{
|
|
21
|
+
MimeTypes.PNG,
|
|
22
|
+
MimeTypes.JPEG,
|
|
23
|
+
MimeTypes.GIF,
|
|
24
|
+
MimeTypes.WEBP,
|
|
25
|
+
MimeTypes.PDF,
|
|
26
|
+
}
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
GOOGLE_SUPPORTED = frozenset(
|
|
30
|
+
{
|
|
31
|
+
*IMAGE_TYPES,
|
|
32
|
+
*DOCUMENT_TYPES,
|
|
33
|
+
*AUDIO_TYPES,
|
|
34
|
+
*VIDEO_TYPES,
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
PROVIDER_FAMILY_SUPPORT: dict[str, frozenset[str]] = {
|
|
39
|
+
"openai": OPENAI_SUPPORTED,
|
|
40
|
+
"anthropic": ANTHROPIC_SUPPORTED,
|
|
41
|
+
"google": GOOGLE_SUPPORTED,
|
|
42
|
+
"azure": OPENAI_SUPPORTED,
|
|
43
|
+
"ollama": IMAGE_TYPES,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_supported_types(provider_family: str) -> frozenset[str]:
|
|
48
|
+
return PROVIDER_FAMILY_SUPPORT.get(provider_family.lower(), IMAGE_TYPES)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def is_supported_by_provider(mime_type: str, provider_family: str) -> bool:
|
|
52
|
+
supported = get_supported_types(provider_family)
|
|
53
|
+
return mime_type in supported
|
|
File without changes
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from spaik_sdk.attachments.models import FileMetadata
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BaseFileStorage(ABC):
|
|
8
|
+
@abstractmethod
|
|
9
|
+
async def store(
|
|
10
|
+
self,
|
|
11
|
+
data: bytes,
|
|
12
|
+
mime_type: str,
|
|
13
|
+
owner_id: str,
|
|
14
|
+
filename: Optional[str] = None,
|
|
15
|
+
) -> FileMetadata:
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
async def retrieve(self, file_id: str) -> tuple[bytes, FileMetadata]:
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
async def get_metadata(self, file_id: str) -> Optional[FileMetadata]:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
async def delete(self, file_id: str) -> bool:
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
async def exists(self, file_id: str) -> bool:
|
|
32
|
+
pass
|
|
File without changes
|