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.
Files changed (161) hide show
  1. spaik_sdk/__init__.py +21 -0
  2. spaik_sdk/agent/__init__.py +0 -0
  3. spaik_sdk/agent/base_agent.py +249 -0
  4. spaik_sdk/attachments/__init__.py +22 -0
  5. spaik_sdk/attachments/builder.py +61 -0
  6. spaik_sdk/attachments/file_storage_provider.py +27 -0
  7. spaik_sdk/attachments/mime_types.py +118 -0
  8. spaik_sdk/attachments/models.py +63 -0
  9. spaik_sdk/attachments/provider_support.py +53 -0
  10. spaik_sdk/attachments/storage/__init__.py +0 -0
  11. spaik_sdk/attachments/storage/base_file_storage.py +32 -0
  12. spaik_sdk/attachments/storage/impl/__init__.py +0 -0
  13. spaik_sdk/attachments/storage/impl/local_file_storage.py +101 -0
  14. spaik_sdk/audio/__init__.py +12 -0
  15. spaik_sdk/audio/options.py +53 -0
  16. spaik_sdk/audio/providers/__init__.py +1 -0
  17. spaik_sdk/audio/providers/google_tts.py +77 -0
  18. spaik_sdk/audio/providers/openai_stt.py +71 -0
  19. spaik_sdk/audio/providers/openai_tts.py +111 -0
  20. spaik_sdk/audio/stt.py +61 -0
  21. spaik_sdk/audio/tts.py +124 -0
  22. spaik_sdk/config/credentials_provider.py +10 -0
  23. spaik_sdk/config/env.py +59 -0
  24. spaik_sdk/config/env_credentials_provider.py +7 -0
  25. spaik_sdk/config/get_credentials_provider.py +14 -0
  26. spaik_sdk/image_gen/__init__.py +9 -0
  27. spaik_sdk/image_gen/image_generator.py +83 -0
  28. spaik_sdk/image_gen/options.py +24 -0
  29. spaik_sdk/image_gen/providers/__init__.py +0 -0
  30. spaik_sdk/image_gen/providers/google.py +75 -0
  31. spaik_sdk/image_gen/providers/openai.py +60 -0
  32. spaik_sdk/llm/__init__.py +0 -0
  33. spaik_sdk/llm/cancellation_handle.py +10 -0
  34. spaik_sdk/llm/consumption/__init__.py +0 -0
  35. spaik_sdk/llm/consumption/consumption_estimate.py +26 -0
  36. spaik_sdk/llm/consumption/consumption_estimate_builder.py +113 -0
  37. spaik_sdk/llm/consumption/consumption_extractor.py +59 -0
  38. spaik_sdk/llm/consumption/token_usage.py +31 -0
  39. spaik_sdk/llm/converters.py +146 -0
  40. spaik_sdk/llm/cost/__init__.py +1 -0
  41. spaik_sdk/llm/cost/builtin_cost_provider.py +83 -0
  42. spaik_sdk/llm/cost/cost_estimate.py +8 -0
  43. spaik_sdk/llm/cost/cost_provider.py +28 -0
  44. spaik_sdk/llm/extract_error_message.py +37 -0
  45. spaik_sdk/llm/langchain_loop_manager.py +270 -0
  46. spaik_sdk/llm/langchain_service.py +196 -0
  47. spaik_sdk/llm/message_handler.py +188 -0
  48. spaik_sdk/llm/streaming/__init__.py +1 -0
  49. spaik_sdk/llm/streaming/block_manager.py +152 -0
  50. spaik_sdk/llm/streaming/models.py +42 -0
  51. spaik_sdk/llm/streaming/streaming_content_handler.py +157 -0
  52. spaik_sdk/llm/streaming/streaming_event_handler.py +215 -0
  53. spaik_sdk/llm/streaming/streaming_state_manager.py +58 -0
  54. spaik_sdk/models/__init__.py +0 -0
  55. spaik_sdk/models/factories/__init__.py +0 -0
  56. spaik_sdk/models/factories/anthropic_factory.py +33 -0
  57. spaik_sdk/models/factories/base_model_factory.py +71 -0
  58. spaik_sdk/models/factories/google_factory.py +30 -0
  59. spaik_sdk/models/factories/ollama_factory.py +41 -0
  60. spaik_sdk/models/factories/openai_factory.py +50 -0
  61. spaik_sdk/models/llm_config.py +46 -0
  62. spaik_sdk/models/llm_families.py +7 -0
  63. spaik_sdk/models/llm_model.py +17 -0
  64. spaik_sdk/models/llm_wrapper.py +25 -0
  65. spaik_sdk/models/model_registry.py +156 -0
  66. spaik_sdk/models/providers/__init__.py +0 -0
  67. spaik_sdk/models/providers/anthropic_provider.py +29 -0
  68. spaik_sdk/models/providers/azure_provider.py +31 -0
  69. spaik_sdk/models/providers/base_provider.py +62 -0
  70. spaik_sdk/models/providers/google_provider.py +26 -0
  71. spaik_sdk/models/providers/ollama_provider.py +26 -0
  72. spaik_sdk/models/providers/openai_provider.py +26 -0
  73. spaik_sdk/models/providers/provider_type.py +90 -0
  74. spaik_sdk/orchestration/__init__.py +24 -0
  75. spaik_sdk/orchestration/base_orchestrator.py +238 -0
  76. spaik_sdk/orchestration/checkpoint.py +80 -0
  77. spaik_sdk/orchestration/models.py +103 -0
  78. spaik_sdk/prompt/__init__.py +0 -0
  79. spaik_sdk/prompt/get_prompt_loader.py +13 -0
  80. spaik_sdk/prompt/local_prompt_loader.py +21 -0
  81. spaik_sdk/prompt/prompt_loader.py +48 -0
  82. spaik_sdk/prompt/prompt_loader_mode.py +14 -0
  83. spaik_sdk/py.typed +1 -0
  84. spaik_sdk/recording/__init__.py +1 -0
  85. spaik_sdk/recording/base_playback.py +90 -0
  86. spaik_sdk/recording/base_recorder.py +50 -0
  87. spaik_sdk/recording/conditional_recorder.py +38 -0
  88. spaik_sdk/recording/impl/__init__.py +1 -0
  89. spaik_sdk/recording/impl/local_playback.py +76 -0
  90. spaik_sdk/recording/impl/local_recorder.py +85 -0
  91. spaik_sdk/recording/langchain_serializer.py +88 -0
  92. spaik_sdk/server/__init__.py +1 -0
  93. spaik_sdk/server/api/routers/__init__.py +0 -0
  94. spaik_sdk/server/api/routers/api_builder.py +149 -0
  95. spaik_sdk/server/api/routers/audio_router_factory.py +201 -0
  96. spaik_sdk/server/api/routers/file_router_factory.py +111 -0
  97. spaik_sdk/server/api/routers/thread_router_factory.py +284 -0
  98. spaik_sdk/server/api/streaming/__init__.py +0 -0
  99. spaik_sdk/server/api/streaming/format_sse_event.py +41 -0
  100. spaik_sdk/server/api/streaming/negotiate_streaming_response.py +8 -0
  101. spaik_sdk/server/api/streaming/streaming_negotiator.py +10 -0
  102. spaik_sdk/server/authorization/__init__.py +0 -0
  103. spaik_sdk/server/authorization/base_authorizer.py +64 -0
  104. spaik_sdk/server/authorization/base_user.py +13 -0
  105. spaik_sdk/server/authorization/dummy_authorizer.py +17 -0
  106. spaik_sdk/server/job_processor/__init__.py +0 -0
  107. spaik_sdk/server/job_processor/base_job_processor.py +8 -0
  108. spaik_sdk/server/job_processor/thread_job_processor.py +32 -0
  109. spaik_sdk/server/pubsub/__init__.py +1 -0
  110. spaik_sdk/server/pubsub/cancellation_publisher.py +7 -0
  111. spaik_sdk/server/pubsub/cancellation_subscriber.py +38 -0
  112. spaik_sdk/server/pubsub/event_publisher.py +13 -0
  113. spaik_sdk/server/pubsub/impl/__init__.py +1 -0
  114. spaik_sdk/server/pubsub/impl/local_cancellation_pubsub.py +48 -0
  115. spaik_sdk/server/pubsub/impl/signalr_publisher.py +36 -0
  116. spaik_sdk/server/queue/__init__.py +1 -0
  117. spaik_sdk/server/queue/agent_job_queue.py +27 -0
  118. spaik_sdk/server/queue/impl/__init__.py +1 -0
  119. spaik_sdk/server/queue/impl/azure_queue.py +24 -0
  120. spaik_sdk/server/response/__init__.py +0 -0
  121. spaik_sdk/server/response/agent_response_generator.py +39 -0
  122. spaik_sdk/server/response/response_generator.py +13 -0
  123. spaik_sdk/server/response/simple_agent_response_generator.py +14 -0
  124. spaik_sdk/server/services/__init__.py +0 -0
  125. spaik_sdk/server/services/thread_converters.py +113 -0
  126. spaik_sdk/server/services/thread_models.py +90 -0
  127. spaik_sdk/server/services/thread_service.py +91 -0
  128. spaik_sdk/server/storage/__init__.py +1 -0
  129. spaik_sdk/server/storage/base_thread_repository.py +51 -0
  130. spaik_sdk/server/storage/impl/__init__.py +0 -0
  131. spaik_sdk/server/storage/impl/in_memory_thread_repository.py +100 -0
  132. spaik_sdk/server/storage/impl/local_file_thread_repository.py +217 -0
  133. spaik_sdk/server/storage/thread_filter.py +166 -0
  134. spaik_sdk/server/storage/thread_metadata.py +53 -0
  135. spaik_sdk/thread/__init__.py +0 -0
  136. spaik_sdk/thread/adapters/__init__.py +0 -0
  137. spaik_sdk/thread/adapters/cli/__init__.py +0 -0
  138. spaik_sdk/thread/adapters/cli/block_display.py +92 -0
  139. spaik_sdk/thread/adapters/cli/display_manager.py +84 -0
  140. spaik_sdk/thread/adapters/cli/live_cli.py +235 -0
  141. spaik_sdk/thread/adapters/event_adapter.py +28 -0
  142. spaik_sdk/thread/adapters/streaming_block_adapter.py +57 -0
  143. spaik_sdk/thread/adapters/sync_adapter.py +76 -0
  144. spaik_sdk/thread/models.py +224 -0
  145. spaik_sdk/thread/thread_container.py +468 -0
  146. spaik_sdk/tools/__init__.py +0 -0
  147. spaik_sdk/tools/impl/__init__.py +0 -0
  148. spaik_sdk/tools/impl/mcp_tool_provider.py +93 -0
  149. spaik_sdk/tools/impl/search_tool_provider.py +18 -0
  150. spaik_sdk/tools/tool_provider.py +131 -0
  151. spaik_sdk/tracing/__init__.py +13 -0
  152. spaik_sdk/tracing/agent_trace.py +72 -0
  153. spaik_sdk/tracing/get_trace_sink.py +15 -0
  154. spaik_sdk/tracing/local_trace_sink.py +23 -0
  155. spaik_sdk/tracing/trace_sink.py +19 -0
  156. spaik_sdk/tracing/trace_sink_mode.py +14 -0
  157. spaik_sdk/utils/__init__.py +0 -0
  158. spaik_sdk/utils/init_logger.py +24 -0
  159. spaik_sdk-0.6.2.dist-info/METADATA +379 -0
  160. spaik_sdk-0.6.2.dist-info/RECORD +161 -0
  161. 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