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
@@ -0,0 +1,48 @@
1
+ from spaik_sdk.server.pubsub.cancellation_publisher import CancellationPublisher
2
+ from spaik_sdk.server.pubsub.cancellation_subscriber import CancellationSubscriber
3
+
4
+
5
+ class LocalCancellationPubsub:
6
+ def __init__(self):
7
+ self.publisher = LocalCancellationPublisher()
8
+
9
+ def create_subscriber(self, id: str) -> CancellationSubscriber:
10
+ return LocalCancellationSubscriber(id, self.publisher)
11
+
12
+ def get_publisher(self) -> CancellationPublisher:
13
+ return self.publisher
14
+
15
+
16
+ class LocalCancellationSubscriber(CancellationSubscriber):
17
+ def __init__(self, id: str, publisher: "LocalCancellationPublisher"):
18
+ self.publisher = publisher
19
+ super().__init__(id)
20
+
21
+ def _subscribe_to_cancellation(self) -> None:
22
+ self.publisher.subscribe(self)
23
+
24
+ def _unsubscribe_from_cancellation(self) -> None:
25
+ self.publisher.unsubscribe(self)
26
+
27
+
28
+ class LocalCancellationPublisher(CancellationPublisher):
29
+ def __init__(self):
30
+ self.subscribers: list[CancellationSubscriber] = []
31
+
32
+ def subscribe(self, subscriber: CancellationSubscriber) -> None:
33
+ self.subscribers.append(subscriber)
34
+
35
+ def unsubscribe(self, subscriber: CancellationSubscriber) -> None:
36
+ self.subscribers.remove(subscriber)
37
+
38
+ def publish_cancellation(self, id: str) -> None:
39
+ for subscriber in self.subscribers:
40
+ if subscriber.id == id:
41
+ subscriber.on_cancellation()
42
+
43
+
44
+ local_cancellation_pubsub = LocalCancellationPubsub()
45
+
46
+
47
+ def get_local_cancellation_pubsub() -> LocalCancellationPubsub:
48
+ return local_cancellation_pubsub
@@ -0,0 +1,36 @@
1
+ import json
2
+ from typing import Any, Dict
3
+
4
+ from spaik_sdk.utils.init_logger import init_logger
5
+
6
+ logger = init_logger(__name__)
7
+
8
+
9
+ # TODO turn into proper events, consider taking it to a different azure lib
10
+ class SignalRPublisher:
11
+ """Azure SignalR publisher"""
12
+
13
+ def __init__(self, config, signalr_out):
14
+ """
15
+ Args:
16
+ signalr_out: Azure Functions SignalR output binding (func.Out[str])
17
+ """
18
+ # super().__init__(config)
19
+ self.signalr_out = signalr_out
20
+
21
+ def publish_event(self, event: Dict[str, Any], job_id: str) -> None:
22
+ """Publish event to Azure SignalR"""
23
+
24
+ # SignalR message format
25
+ signalr_message = {
26
+ "target": "agentEvent", # Client-side method name
27
+ "arguments": [event],
28
+ }
29
+
30
+ # Send to SignalR
31
+ message_json = json.dumps(signalr_message)
32
+ self.signalr_out.set(message_json)
33
+
34
+ job_id = event.get("job_id", "unknown")
35
+ event_type = event.get("event_type", "unknown")
36
+ logger.debug(f"Published {event_type} to SignalR for job {job_id}")
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,27 @@
1
+ from abc import ABC, abstractmethod
2
+ from enum import Enum
3
+
4
+ from pydantic import BaseModel
5
+
6
+
7
+ class JobType(Enum):
8
+ THREAD_MESSAGE = "thread_message"
9
+ ORCHESTRATION = "orchestration"
10
+
11
+
12
+ class AgentJob(BaseModel):
13
+ id: str
14
+ job_type: JobType
15
+
16
+ def to_json(self) -> str:
17
+ """Convert AgentJob to JSON string"""
18
+ return self.model_dump_json()
19
+
20
+
21
+ class AgentJobQueue(ABC):
22
+ """Minimal queue abstraction - just push jobs"""
23
+
24
+ @abstractmethod
25
+ async def push(self, job: AgentJob) -> None:
26
+ """Push job to queue"""
27
+ pass
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,24 @@
1
+ from typing import Any
2
+
3
+ from spaik_sdk.server.queue.agent_job_queue import AgentJob
4
+ from spaik_sdk.utils.init_logger import init_logger
5
+
6
+ logger = init_logger(__name__)
7
+
8
+
9
+ class AzureFunctionQueue:
10
+ """Azure Functions queue implementation"""
11
+
12
+ def __init__(self, func_out: Any):
13
+ """
14
+ Args:
15
+ func_out: Azure Functions queue output binding (func.Out[str])
16
+ """
17
+ self.func_out = func_out
18
+
19
+ async def push(self, job: AgentJob) -> None:
20
+ """Push job to Azure Functions queue"""
21
+ json_data = job.to_json()
22
+
23
+ # Set the message in the Azure Functions queue output
24
+ self.func_out.set(json_data)
File without changes
@@ -0,0 +1,39 @@
1
+ import time
2
+ from collections.abc import Callable
3
+ from typing import Any, AsyncGenerator, Dict, Optional, TypeVar
4
+
5
+ from spaik_sdk.agent.base_agent import BaseAgent
6
+ from spaik_sdk.llm.cancellation_handle import CancellationHandle
7
+ from spaik_sdk.server.response.response_generator import ResponseGenerator
8
+ from spaik_sdk.thread.models import ThreadEvent
9
+ from spaik_sdk.thread.thread_container import ThreadContainer
10
+
11
+ TAgent = TypeVar("TAgent", bound=BaseAgent)
12
+
13
+
14
+ class AgentResponseGenerator(ResponseGenerator):
15
+ def __init__(self, agent: TAgent, call_agent: Callable[[TAgent], AsyncGenerator[ThreadEvent, None]]):
16
+ self.agent = agent
17
+ self.call_agent = call_agent
18
+
19
+ async def stream_response(
20
+ self, thread: ThreadContainer, cancellation_handle: Optional[CancellationHandle] = None
21
+ ) -> AsyncGenerator[Dict[str, Any], None]:
22
+ self.agent.set_thread_container(thread)
23
+ self.agent.set_cancellation_handle(cancellation_handle)
24
+
25
+ # Stream from the agent execution
26
+ async for event in self.call_agent(self.agent):
27
+ if not event.is_publishable():
28
+ continue
29
+ yield self._convert_event(event, thread.thread_id)
30
+
31
+ def _convert_event(self, event: ThreadEvent, thread_id: str) -> Dict[str, Any]:
32
+ """Convert ThreadContainer event to publishable format"""
33
+
34
+ return {
35
+ "thread_id": thread_id,
36
+ "event_type": event.get_event_type(),
37
+ "timestamp": int(time.time() * 1000),
38
+ "data": event.get_event_data(),
39
+ }
@@ -0,0 +1,13 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any, AsyncGenerator, Dict, Optional
3
+
4
+ from spaik_sdk.llm.cancellation_handle import CancellationHandle
5
+ from spaik_sdk.thread.thread_container import ThreadContainer
6
+
7
+
8
+ class ResponseGenerator(ABC):
9
+ @abstractmethod
10
+ def stream_response(
11
+ self, thread: ThreadContainer, cancellation_handle: Optional[CancellationHandle] = None
12
+ ) -> AsyncGenerator[Dict[str, Any], None]:
13
+ pass
@@ -0,0 +1,14 @@
1
+ from typing import AsyncGenerator
2
+
3
+ from spaik_sdk.agent.base_agent import BaseAgent
4
+ from spaik_sdk.server.response.agent_response_generator import AgentResponseGenerator
5
+ from spaik_sdk.thread.models import ThreadEvent
6
+
7
+
8
+ class SimpleAgentResponseGenerator(AgentResponseGenerator):
9
+ def __init__(self, agent: BaseAgent):
10
+ async def call_agent(agent: BaseAgent) -> AsyncGenerator[ThreadEvent, None]:
11
+ async for event in agent.get_event_stream():
12
+ yield event
13
+
14
+ super().__init__(agent, call_agent)
File without changes
@@ -0,0 +1,113 @@
1
+ import uuid
2
+ from typing import List
3
+
4
+ from spaik_sdk.attachments.models import Attachment
5
+ from spaik_sdk.server.services.thread_models import (
6
+ AttachmentRequest,
7
+ AttachmentResponse,
8
+ MessageBlockRequest,
9
+ MessageBlockResponse,
10
+ MessageResponse,
11
+ ThreadMetadataResponse,
12
+ ThreadResponse,
13
+ )
14
+ from spaik_sdk.server.storage.thread_metadata import ThreadMetadata
15
+ from spaik_sdk.thread.models import MessageBlock, ThreadMessage
16
+ from spaik_sdk.thread.thread_container import ThreadContainer
17
+
18
+
19
+ class ThreadConverters:
20
+ """Utility class for converting between internal and API models"""
21
+
22
+ @staticmethod
23
+ def block_request_to_model(request: MessageBlockRequest) -> MessageBlock:
24
+ """Convert MessageBlockRequest to MessageBlock"""
25
+ return MessageBlock(
26
+ id=str(uuid.uuid4()),
27
+ streaming=False,
28
+ type=request.type,
29
+ content=request.content,
30
+ tool_call_id=request.tool_call_id,
31
+ tool_call_args=request.tool_call_args,
32
+ tool_name=request.tool_name,
33
+ )
34
+
35
+ @staticmethod
36
+ def block_model_to_response(block: MessageBlock) -> MessageBlockResponse:
37
+ """Convert MessageBlock to MessageBlockResponse"""
38
+ return MessageBlockResponse(
39
+ id=block.id,
40
+ streaming=block.streaming,
41
+ type=block.type,
42
+ content=block.content,
43
+ tool_call_id=block.tool_call_id,
44
+ tool_call_args=block.tool_call_args,
45
+ tool_name=block.tool_name,
46
+ tool_call_response=block.tool_call_response,
47
+ tool_call_error=block.tool_call_error,
48
+ )
49
+
50
+ @staticmethod
51
+ def attachment_request_to_model(request: AttachmentRequest) -> Attachment:
52
+ """Convert AttachmentRequest to Attachment"""
53
+ return Attachment(
54
+ file_id=request.file_id,
55
+ mime_type=request.mime_type,
56
+ filename=request.filename,
57
+ )
58
+
59
+ @staticmethod
60
+ def attachment_model_to_response(attachment: Attachment) -> AttachmentResponse:
61
+ """Convert Attachment to AttachmentResponse"""
62
+ return AttachmentResponse(
63
+ file_id=attachment.file_id,
64
+ mime_type=attachment.mime_type,
65
+ filename=attachment.filename,
66
+ )
67
+
68
+ @staticmethod
69
+ def message_model_to_response(message: ThreadMessage) -> MessageResponse:
70
+ """Convert ThreadMessage to MessageResponse"""
71
+ attachments = None
72
+ if message.attachments:
73
+ attachments = [ThreadConverters.attachment_model_to_response(att) for att in message.attachments]
74
+ return MessageResponse(
75
+ id=message.id,
76
+ ai=message.ai,
77
+ author_id=message.author_id,
78
+ author_name=message.author_name,
79
+ timestamp=message.timestamp,
80
+ blocks=[ThreadConverters.block_model_to_response(block) for block in message.blocks],
81
+ attachments=attachments,
82
+ )
83
+
84
+ @staticmethod
85
+ def messages_model_to_response(messages: List[ThreadMessage]) -> List[MessageResponse]:
86
+ """Convert list of ThreadMessage to list of MessageResponse"""
87
+ return [ThreadConverters.message_model_to_response(msg) for msg in messages]
88
+
89
+ @staticmethod
90
+ def thread_model_to_response(thread: ThreadContainer) -> ThreadResponse:
91
+ """Convert ThreadContainer to ThreadResponse"""
92
+ return ThreadResponse(
93
+ id=thread.thread_id,
94
+ messages=[ThreadConverters.message_model_to_response(msg) for msg in thread.messages],
95
+ )
96
+
97
+ @staticmethod
98
+ def metadata_model_to_response(metadata: ThreadMetadata) -> ThreadMetadataResponse:
99
+ """Convert ThreadMetadata to ThreadMetadataResponse"""
100
+ return ThreadMetadataResponse(
101
+ thread_id=metadata.thread_id,
102
+ title=metadata.title,
103
+ message_count=metadata.message_count,
104
+ last_activity_time=metadata.last_activity_time,
105
+ created_at=metadata.created_at,
106
+ author_id=metadata.author_id,
107
+ type=metadata.type,
108
+ )
109
+
110
+ @staticmethod
111
+ def metadata_list_to_response(metadata_list: List[ThreadMetadata]) -> List[ThreadMetadataResponse]:
112
+ """Convert list of ThreadMetadata to list of ThreadMetadataResponse"""
113
+ return [ThreadConverters.metadata_model_to_response(metadata) for metadata in metadata_list]
@@ -0,0 +1,90 @@
1
+ from typing import Any, Dict, List, Optional
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from spaik_sdk.thread.models import MessageBlockType
6
+
7
+
8
+ class MessageBlockRequest(BaseModel):
9
+ type: MessageBlockType
10
+ content: Optional[str] = None
11
+ tool_call_id: Optional[str] = None
12
+ tool_call_args: Optional[Dict[str, Any]] = None
13
+ tool_name: Optional[str] = None
14
+
15
+
16
+ class MessageBlockResponse(BaseModel):
17
+ id: str
18
+ streaming: bool
19
+ type: MessageBlockType
20
+ content: Optional[str] = None
21
+ tool_call_id: Optional[str] = None
22
+ tool_call_args: Optional[Dict[str, Any]] = None
23
+ tool_name: Optional[str] = None
24
+ tool_call_response: Optional[str] = None
25
+ tool_call_error: Optional[str] = None
26
+
27
+
28
+ class UpdateMessageRequest(BaseModel):
29
+ blocks: Optional[List[MessageBlockRequest]] = None
30
+
31
+
32
+ class AttachmentRequest(BaseModel):
33
+ file_id: str
34
+ mime_type: str
35
+ filename: Optional[str] = None
36
+
37
+
38
+ class AttachmentResponse(BaseModel):
39
+ file_id: str
40
+ mime_type: str
41
+ filename: Optional[str] = None
42
+
43
+
44
+ class MessageResponse(BaseModel):
45
+ id: str
46
+ ai: bool
47
+ author_id: str
48
+ author_name: str
49
+ timestamp: int
50
+ blocks: List[MessageBlockResponse]
51
+ attachments: Optional[List[AttachmentResponse]] = None
52
+
53
+
54
+ class CreateMessageRequest(BaseModel):
55
+ content: str
56
+ attachments: Optional[List[AttachmentRequest]] = None
57
+
58
+
59
+ class CreateThreadRequest(BaseModel):
60
+ job_id: Optional[str] = "unknown"
61
+ # TODO add metadata, remove job_id
62
+
63
+
64
+ class ThreadResponse(BaseModel):
65
+ id: str
66
+ messages: List[MessageResponse]
67
+
68
+
69
+ class ThreadMetadataResponse(BaseModel):
70
+ thread_id: str
71
+ title: str
72
+ message_count: int
73
+ last_activity_time: int
74
+ created_at: int
75
+ author_id: str
76
+ type: str
77
+
78
+
79
+ class ListThreadsRequest(BaseModel):
80
+ author_id: Optional[str] = None
81
+ thread_type: Optional[str] = None
82
+ title_contains: Optional[str] = None
83
+ min_messages: Optional[int] = None
84
+ max_messages: Optional[int] = None
85
+ hours_ago: Optional[int] = None
86
+
87
+
88
+ class ListThreadsResponse(BaseModel):
89
+ threads: List[ThreadMetadataResponse]
90
+ total_count: int
@@ -0,0 +1,91 @@
1
+ from typing import List, Optional
2
+
3
+ from spaik_sdk.server.storage.base_thread_repository import BaseThreadRepository
4
+ from spaik_sdk.server.storage.thread_filter import ThreadFilter
5
+ from spaik_sdk.server.storage.thread_metadata import ThreadMetadata
6
+ from spaik_sdk.thread.models import ThreadMessage
7
+ from spaik_sdk.thread.thread_container import ThreadContainer
8
+
9
+
10
+ class ThreadService:
11
+ """High-level service for thread and message operations"""
12
+
13
+ def __init__(self, repository: BaseThreadRepository):
14
+ self.repository = repository
15
+
16
+ # Thread operations
17
+ async def create_thread(self, thread_container: ThreadContainer) -> ThreadContainer:
18
+ """Create a new thread"""
19
+ await self.repository.save_thread(thread_container)
20
+ return thread_container
21
+
22
+ async def get_thread(self, thread_id: str) -> Optional[ThreadContainer]:
23
+ """Get thread by ID"""
24
+ return await self.repository.load_thread(thread_id)
25
+
26
+ async def update_thread(self, thread_container: ThreadContainer) -> Optional[ThreadContainer]:
27
+ """Update an existing thread"""
28
+ if not await self.repository.thread_exists(thread_container.thread_id):
29
+ return None
30
+
31
+ await self.repository.save_thread(thread_container)
32
+ return thread_container
33
+
34
+ async def delete_thread(self, thread_id: str) -> bool:
35
+ """Delete thread and all its messages"""
36
+ return await self.repository.delete_thread(thread_id)
37
+
38
+ async def thread_exists(self, thread_id: str) -> bool:
39
+ """Check if thread exists"""
40
+ return await self.repository.thread_exists(thread_id)
41
+
42
+ async def list_threads(self, filter: ThreadFilter) -> List[ThreadMetadata]:
43
+ """List threads matching filter"""
44
+ return await self.repository.list_threads(filter)
45
+
46
+ # Message operations
47
+ async def get_message(self, thread_id: str, message_id: str) -> Optional[ThreadMessage]:
48
+ """Get message by thread ID and message ID"""
49
+ return await self.repository.get_message(thread_id, message_id)
50
+
51
+ async def add_message(self, thread_id: str, message: ThreadMessage) -> Optional[ThreadMessage]:
52
+ """Add message to thread"""
53
+ if not await self.repository.thread_exists(thread_id):
54
+ return None
55
+
56
+ await self.repository.upsert_message(thread_id, message)
57
+ return message
58
+
59
+ async def update_message(self, thread_id: str, message: ThreadMessage) -> Optional[ThreadMessage]:
60
+ """Update existing message in thread"""
61
+ if not await self.repository.thread_exists(thread_id):
62
+ return None
63
+
64
+ # Check if message exists
65
+ existing_message = await self.repository.get_message(thread_id, message.id)
66
+ if not existing_message:
67
+ return None
68
+
69
+ await self.repository.upsert_message(thread_id, message)
70
+ return message
71
+
72
+ async def delete_message(self, thread_id: str, message_id: str) -> bool:
73
+ """Delete message from thread"""
74
+ if not await self.repository.thread_exists(thread_id):
75
+ return False
76
+
77
+ # Check if message exists
78
+ existing_message = await self.repository.get_message(thread_id, message_id)
79
+ if not existing_message:
80
+ return False
81
+
82
+ await self.repository.delete_message(thread_id, message_id)
83
+ return True
84
+
85
+ async def get_thread_messages(self, thread_id: str) -> List[ThreadMessage]:
86
+ """Get all messages for a thread"""
87
+ thread = await self.repository.load_thread(thread_id)
88
+ if not thread:
89
+ return []
90
+
91
+ return thread.messages
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,51 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import List, Optional
3
+
4
+ from spaik_sdk.server.storage.thread_filter import ThreadFilter
5
+ from spaik_sdk.server.storage.thread_metadata import ThreadMetadata
6
+ from spaik_sdk.thread.models import ThreadMessage
7
+ from spaik_sdk.thread.thread_container import ThreadContainer
8
+
9
+
10
+ class BaseThreadRepository(ABC):
11
+ """Abstract base class for thread persistence"""
12
+
13
+ @abstractmethod
14
+ async def save_thread(self, thread_container: ThreadContainer) -> None:
15
+ """Save complete thread container to storage"""
16
+ pass
17
+
18
+ @abstractmethod
19
+ async def load_thread(self, thread_id: str) -> Optional[ThreadContainer]:
20
+ """Load thread container from storage"""
21
+ pass
22
+
23
+ @abstractmethod
24
+ async def get_message(self, thread_id: str, message_id: str) -> Optional[ThreadMessage]:
25
+ """Get message from storage"""
26
+ pass
27
+
28
+ @abstractmethod
29
+ async def upsert_message(self, thread_id: str, message: ThreadMessage) -> None:
30
+ """Upsert message to storage"""
31
+ pass
32
+
33
+ @abstractmethod
34
+ async def delete_message(self, thread_id: str, message_id: str) -> None:
35
+ """Delete message from storage"""
36
+ pass
37
+
38
+ @abstractmethod
39
+ async def thread_exists(self, thread_id: str) -> bool:
40
+ """Check if thread exists"""
41
+ pass
42
+
43
+ @abstractmethod
44
+ async def delete_thread(self, thread_id: str) -> bool:
45
+ """Delete thread and all its messages"""
46
+ pass
47
+
48
+ @abstractmethod
49
+ async def list_threads(self, filter: ThreadFilter) -> List[ThreadMetadata]:
50
+ """List threads for a given filter"""
51
+ pass
File without changes
@@ -0,0 +1,100 @@
1
+ from typing import Dict, List, Optional
2
+
3
+ from spaik_sdk.server.storage.base_thread_repository import BaseThreadRepository
4
+ from spaik_sdk.server.storage.thread_filter import ThreadFilter
5
+ from spaik_sdk.server.storage.thread_metadata import ThreadMetadata
6
+ from spaik_sdk.thread.models import ThreadMessage
7
+ from spaik_sdk.thread.thread_container import ThreadContainer
8
+
9
+
10
+ class InMemoryThreadRepository(BaseThreadRepository):
11
+ """In-memory implementation of thread repository for testing/development"""
12
+
13
+ def __init__(self):
14
+ self._threads: Dict[str, ThreadContainer] = {}
15
+ self._metadata: Dict[str, ThreadMetadata] = {}
16
+
17
+ async def save_thread(self, thread_container: ThreadContainer) -> None:
18
+ """Save complete thread container to memory"""
19
+ self._threads[thread_container.thread_id] = thread_container
20
+ # Update metadata
21
+ metadata = ThreadMetadata.from_thread_container(thread_container)
22
+ self._metadata[thread_container.thread_id] = metadata
23
+
24
+ async def load_thread(self, thread_id: str) -> Optional[ThreadContainer]:
25
+ """Load thread container from memory"""
26
+ return self._threads.get(thread_id)
27
+
28
+ async def get_message(self, thread_id: str, message_id: str) -> Optional[ThreadMessage]:
29
+ """Get message from memory"""
30
+ thread = self._threads.get(thread_id)
31
+ if not thread:
32
+ return None
33
+
34
+ for message in thread.messages:
35
+ if message.id == message_id:
36
+ return message
37
+ return None
38
+
39
+ async def upsert_message(self, thread_id: str, message: ThreadMessage) -> None:
40
+ """Upsert message to memory"""
41
+ thread = self._threads.get(thread_id)
42
+ if not thread:
43
+ return
44
+
45
+ # Find existing message and replace, or add new one
46
+ for i, existing_msg in enumerate(thread.messages):
47
+ if existing_msg.id == message.id:
48
+ thread.messages[i] = message
49
+ break
50
+ else:
51
+ thread.messages.append(message)
52
+
53
+ # Update metadata
54
+ metadata = ThreadMetadata.from_thread_container(thread)
55
+ self._metadata[thread_id] = metadata
56
+
57
+ async def delete_message(self, thread_id: str, message_id: str) -> None:
58
+ """Delete message from memory"""
59
+ thread = self._threads.get(thread_id)
60
+ if not thread:
61
+ return
62
+
63
+ thread.messages = [msg for msg in thread.messages if msg.id != message_id]
64
+
65
+ # Update metadata
66
+ metadata = ThreadMetadata.from_thread_container(thread)
67
+ self._metadata[thread_id] = metadata
68
+
69
+ async def thread_exists(self, thread_id: str) -> bool:
70
+ """Check if thread exists in memory"""
71
+ return thread_id in self._threads
72
+
73
+ async def delete_thread(self, thread_id: str) -> bool:
74
+ """Delete thread and all its messages from memory"""
75
+ if thread_id in self._threads:
76
+ del self._threads[thread_id]
77
+ if thread_id in self._metadata:
78
+ del self._metadata[thread_id]
79
+ return True
80
+ return False
81
+
82
+ async def list_threads(self, filter: ThreadFilter) -> List[ThreadMetadata]:
83
+ """List threads matching the filter from memory"""
84
+ result = []
85
+ for metadata in self._metadata.values():
86
+ if filter.matches(metadata):
87
+ result.append(metadata)
88
+
89
+ # Sort by last activity time (most recent first)
90
+ result.sort(key=lambda x: x.last_activity_time, reverse=True)
91
+ return result
92
+
93
+ def clear_all(self) -> None:
94
+ """Clear all data (useful for testing)"""
95
+ self._threads.clear()
96
+ self._metadata.clear()
97
+
98
+ def get_thread_count(self) -> int:
99
+ """Get total number of threads stored"""
100
+ return len(self._threads)