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,131 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any, Callable, Dict, List, Optional, Tuple, Union
3
+
4
+ from langchain_core.tools import BaseTool, StructuredTool, tool
5
+ from pydantic import BaseModel, Field, create_model
6
+
7
+
8
+ class ToolProvider(ABC):
9
+ """
10
+ Abstract class for tool providers.
11
+ """
12
+
13
+ @abstractmethod
14
+ def get_tools(self) -> List[BaseTool]:
15
+ """
16
+ Returns a Langchain BaseTool implementation.
17
+
18
+ Returns:
19
+ BaseTool: A Langchain tool
20
+ """
21
+ pass
22
+
23
+ @staticmethod
24
+ def create_tool(
25
+ func: Callable[..., Any], name: str, description: str, args_schema: Optional[type[BaseModel]] = None, **kwargs
26
+ ) -> BaseTool:
27
+ """
28
+ Create a tool with customizable name and description.
29
+
30
+ Args:
31
+ func: The function to wrap as a tool
32
+ name: The tool name
33
+ description: The tool description (overrides docstring)
34
+ args_schema: Optional Pydantic model for argument validation
35
+ **kwargs: Additional arguments passed to StructuredTool.from_function
36
+
37
+ Returns:
38
+ BaseTool: A configured langchain tool
39
+
40
+ Example:
41
+ def my_func(text: str) -> str:
42
+ return f"Processed: {text}"
43
+
44
+ # Basic usage
45
+ tool = create_tool(my_func, "process", "Process text input")
46
+
47
+ # With custom schema
48
+ class MyArgs(BaseModel):
49
+ text: str = Field(description="Text to process")
50
+
51
+ tool = create_tool(my_func, "process", "Process text", MyArgs)
52
+ """
53
+ return StructuredTool.from_function(func=func, name=name, description=description, args_schema=args_schema, **kwargs)
54
+
55
+ @staticmethod
56
+ def create_dynamic_tool(
57
+ func: Callable[..., Any], name: str, description: str, args: Optional[Dict[str, Union[type, Tuple[type, str]]]] = None, **kwargs
58
+ ) -> BaseTool:
59
+ """
60
+ Create a tool with dynamic argument schema.
61
+
62
+ Args:
63
+ func: The function to wrap as a tool
64
+ name: The tool name
65
+ description: The tool description
66
+ args: Dict mapping arg names to types or (type, description) tuples
67
+ **kwargs: Additional arguments passed to StructuredTool.from_function
68
+
69
+ Returns:
70
+ BaseTool: A configured langchain tool
71
+
72
+ Example:
73
+ def search(query: str, limit: int = 10) -> str:
74
+ return f"Found results for {query}"
75
+
76
+ tool = create_dynamic_tool(
77
+ search,
78
+ "search_tool",
79
+ "Search for information",
80
+ args={
81
+ "query": (str, "The search query"),
82
+ "limit": (int, "Max results (default: 10)")
83
+ }
84
+ )
85
+ """
86
+ args_schema = None
87
+ if args:
88
+ # Build field definitions for dynamic schema
89
+ fields = {}
90
+ for arg_name, arg_spec in args.items():
91
+ if isinstance(arg_spec, tuple):
92
+ arg_type, arg_desc = arg_spec
93
+ fields[arg_name] = (arg_type, Field(description=arg_desc))
94
+ else:
95
+ fields[arg_name] = (arg_spec, Field())
96
+
97
+ # Create dynamic Pydantic model
98
+ args_schema = create_model(f"{name}Args", **fields) # type: ignore
99
+
100
+ return StructuredTool.from_function(func=func, name=name, description=description, args_schema=args_schema, **kwargs)
101
+
102
+ @staticmethod
103
+ def create_simple_tool(func: Callable[..., Any], name: str, description: str, **kwargs) -> BaseTool:
104
+ """
105
+ Create a simple tool without argument validation.
106
+
107
+ Args:
108
+ func: The function to wrap as a tool
109
+ name: The tool name
110
+ description: The tool description
111
+ **kwargs: Additional arguments passed to StructuredTool.from_function
112
+
113
+ Returns:
114
+ BaseTool: A configured langchain tool
115
+
116
+ Example:
117
+ def get_time() -> str:
118
+ import datetime
119
+ return datetime.datetime.now().isoformat()
120
+
121
+ tool = create_simple_tool(
122
+ get_time,
123
+ "current_time",
124
+ "Get current timestamp"
125
+ )
126
+ """
127
+ return StructuredTool.from_function(func=func, name=name, description=description, **kwargs)
128
+
129
+
130
+ # Re-export langchain tools for convenience
131
+ __all__ = ["ToolProvider", "BaseTool", "tool"]
@@ -0,0 +1,13 @@
1
+ from spaik_sdk.tracing.agent_trace import AgentTrace
2
+ from spaik_sdk.tracing.get_trace_sink import get_trace_sink
3
+ from spaik_sdk.tracing.local_trace_sink import LocalTraceSink
4
+ from spaik_sdk.tracing.trace_sink import TraceSink
5
+ from spaik_sdk.tracing.trace_sink_mode import TraceSinkMode
6
+
7
+ __all__ = [
8
+ "AgentTrace",
9
+ "TraceSink",
10
+ "LocalTraceSink",
11
+ "TraceSinkMode",
12
+ "get_trace_sink",
13
+ ]
@@ -0,0 +1,72 @@
1
+ import json
2
+ import time
3
+ from typing import Optional, Type
4
+
5
+ from pydantic import BaseModel
6
+
7
+ from spaik_sdk.thread.models import MessageBlock, MessageBlockType
8
+ from spaik_sdk.tracing.get_trace_sink import get_trace_sink
9
+ from spaik_sdk.tracing.trace_sink import TraceSink
10
+
11
+
12
+ class AgentTrace:
13
+ def __init__(
14
+ self,
15
+ system_prompt: str,
16
+ save_name: Optional[str] = None,
17
+ trace_sink: Optional[TraceSink] = None,
18
+ ):
19
+ self.system_prompt: str = system_prompt
20
+ self._start_time_monotonic: float = time.monotonic()
21
+ self._steps: list[tuple[float, str]] = []
22
+ self.save_name: Optional[str] = save_name
23
+ self._trace_sink: TraceSink = trace_sink or get_trace_sink()
24
+
25
+ def add_step(self, step_content: str) -> None:
26
+ current_time_monotonic: float = time.monotonic()
27
+ elapsed_time: float = current_time_monotonic - self._start_time_monotonic
28
+ self._steps.append((elapsed_time, step_content))
29
+ if self.save_name is not None:
30
+ self.save(self.save_name)
31
+
32
+ def add_structured_response_input(self, input: str, model: Type[BaseModel]) -> None:
33
+ self.add_step(f"📄: {input} \n {json.dumps(model.model_json_schema(), indent=2)}")
34
+
35
+ def add_structured_response_output(self, output: BaseModel) -> None:
36
+ self.add_step(f"```json\n{json.dumps(output.model_dump(), indent=2)}\n```")
37
+
38
+ def add_input(self, input: Optional[str] = None) -> None:
39
+ if input is None:
40
+ return
41
+ self.add_step(f"👤: {input}")
42
+
43
+ def add_block(self, block: MessageBlock) -> None:
44
+ if block.type == MessageBlockType.PLAIN:
45
+ self.add_step(f"🤖: {block.content}")
46
+ elif block.type == MessageBlockType.REASONING:
47
+ self.add_step(f"🧠: {block.content}")
48
+ elif block.type == MessageBlockType.TOOL_USE:
49
+ self.add_step(f"🔧: {block.tool_name} {json.dumps(block.tool_call_args, indent=2)}")
50
+ elif block.type == MessageBlockType.ERROR:
51
+ self.add_step(f"🚨: {block.content}")
52
+ else:
53
+ self.add_step(f"❓: {block.content}")
54
+
55
+ def to_string(self, include_system_prompt: bool = True) -> str:
56
+ lines: list[str] = []
57
+ if include_system_prompt:
58
+ lines.append("[system prompt]")
59
+ lines.append("")
60
+ lines.append(self.system_prompt)
61
+
62
+ for elapsed_seconds, content in self._steps:
63
+ lines.append("")
64
+ lines.append(f"[{elapsed_seconds:.1f}s]")
65
+ lines.append("")
66
+ lines.append(content)
67
+
68
+ return "\n".join(lines)
69
+
70
+ def save(self, name: str) -> None:
71
+ trace_content = self.to_string(include_system_prompt=False)
72
+ self._trace_sink.save_trace(name, trace_content, self.system_prompt)
@@ -0,0 +1,15 @@
1
+ from typing import Optional
2
+
3
+ from spaik_sdk.tracing.local_trace_sink import LocalTraceSink
4
+ from spaik_sdk.tracing.trace_sink import TraceSink
5
+ from spaik_sdk.tracing.trace_sink_mode import TraceSinkMode
6
+
7
+
8
+ def get_trace_sink(mode: Optional[TraceSinkMode] = None) -> TraceSink:
9
+ # Lazy import to avoid circular dependency with env.py
10
+ from spaik_sdk.config.env import env_config
11
+
12
+ mode = mode or env_config.get_trace_sink_mode()
13
+ if mode == TraceSinkMode.LOCAL:
14
+ return LocalTraceSink()
15
+ raise ValueError(f"Unknown TraceSinkMode: {mode}")
@@ -0,0 +1,23 @@
1
+ import os
2
+ from typing import Optional
3
+
4
+ from spaik_sdk.tracing.trace_sink import TraceSink
5
+
6
+
7
+ class LocalTraceSink(TraceSink):
8
+ """TraceSink implementation that writes traces to the local filesystem."""
9
+
10
+ def __init__(self, traces_dir: Optional[str] = None):
11
+ self.traces_dir = traces_dir or "traces"
12
+
13
+ def save_trace(self, name: str, trace_content: str, system_prompt: str) -> None:
14
+ os.makedirs(self.traces_dir, exist_ok=True)
15
+
16
+ trace_path = os.path.join(self.traces_dir, f"{name}.txt")
17
+ system_prompt_path = os.path.join(self.traces_dir, f"{name}_system_prompt.txt")
18
+
19
+ with open(trace_path, "w", encoding="utf-8") as f:
20
+ f.write(trace_content)
21
+
22
+ with open(system_prompt_path, "w", encoding="utf-8") as f:
23
+ f.write(system_prompt)
@@ -0,0 +1,19 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+
4
+ class TraceSink(ABC):
5
+ """Abstract base class for trace storage backends.
6
+
7
+ Implementations can write traces to local files, remote APIs, databases, etc.
8
+ """
9
+
10
+ @abstractmethod
11
+ def save_trace(self, name: str, trace_content: str, system_prompt: str) -> None:
12
+ """Save a trace with its system prompt.
13
+
14
+ Args:
15
+ name: Identifier for the trace (e.g., agent class name)
16
+ trace_content: The formatted trace content (without system prompt)
17
+ system_prompt: The system prompt used for the agent
18
+ """
19
+ pass
@@ -0,0 +1,14 @@
1
+ from enum import Enum
2
+
3
+
4
+ class TraceSinkMode(Enum):
5
+ LOCAL = "local"
6
+
7
+ @classmethod
8
+ def from_name(cls, name: str) -> "TraceSinkMode":
9
+ for mode in cls:
10
+ if mode.value == name:
11
+ return mode
12
+
13
+ available_modes = [mode.value for mode in cls]
14
+ raise ValueError(f"Unknown TraceSinkMode '{name}'. Available: {', '.join(available_modes)}")
File without changes
@@ -0,0 +1,24 @@
1
+ import logging
2
+ import os
3
+ from typing import cast
4
+
5
+ LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO").upper()
6
+ TRACE_LEVEL = 5
7
+ logging.addLevelName(TRACE_LEVEL, "TRACE")
8
+
9
+
10
+ class CustomLogger(logging.Logger):
11
+ def trace(self, msg, *args, **kwargs):
12
+ if self.isEnabledFor(TRACE_LEVEL):
13
+ self._log(TRACE_LEVEL, msg, args, **kwargs)
14
+
15
+
16
+ def init_logger(name: str) -> CustomLogger:
17
+ logging.setLoggerClass(CustomLogger)
18
+ logging.basicConfig(
19
+ level=getattr(logging, LOG_LEVEL, logging.INFO if LOG_LEVEL == "INFO" else logging.DEBUG),
20
+ format="%(asctime)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s",
21
+ force=True, # This ensures we override any existing config
22
+ )
23
+ logger = logging.getLogger(name)
24
+ return cast(CustomLogger, logger)
@@ -0,0 +1,379 @@
1
+ Metadata-Version: 2.4
2
+ Name: spaik-sdk
3
+ Version: 0.6.2
4
+ Summary: Python SDK for building AI agents with multi-LLM support, streaming, and production-ready infrastructure
5
+ Project-URL: Homepage, https://github.com/siilisolutions/spaik-sdk
6
+ Project-URL: Repository, https://github.com/siilisolutions/spaik-sdk
7
+ Project-URL: Documentation, https://github.com/siilisolutions/spaik-sdk#readme
8
+ Author-email: Siili Solutions Oyj <info@siili.com>
9
+ License-Expression: MIT
10
+ Keywords: agents,ai,anthropic,claude,gpt,langchain,llm,openai,streaming
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
19
+ Requires-Python: >=3.10
20
+ Requires-Dist: aioconsole>=0.8.1
21
+ Requires-Dist: azure-storage-blob
22
+ Requires-Dist: cryptography>=41.0.0
23
+ Requires-Dist: dotenv>=0.9.9
24
+ Requires-Dist: fastapi>=0.115.12
25
+ Requires-Dist: httpx>=0.25.0
26
+ Requires-Dist: langchain-anthropic>=1.3.0
27
+ Requires-Dist: langchain-core>=1.2.0
28
+ Requires-Dist: langchain-google-genai>=4.0.0
29
+ Requires-Dist: langchain-mcp-adapters>=0.2.1
30
+ Requires-Dist: langchain-ollama>=0.3.0
31
+ Requires-Dist: langchain-openai>=1.1.0
32
+ Requires-Dist: langchain-tavily>=0.2.15
33
+ Requires-Dist: langchain>=1.2.0
34
+ Requires-Dist: langgraph>=1.0.0
35
+ Requires-Dist: mcp>=1.9.2
36
+ Requires-Dist: pandas-stubs
37
+ Requires-Dist: pandas>=2.0.3
38
+ Requires-Dist: pyjwt>=2.8.0
39
+ Requires-Dist: pytest-asyncio>=0.21.1
40
+ Requires-Dist: pytest-cov>=4.1.0
41
+ Requires-Dist: pytest-mock>=3.11.1
42
+ Requires-Dist: pytest>=7.4.0
43
+ Requires-Dist: requests>=2.31.0
44
+ Requires-Dist: restrictedpython>=8.0
45
+ Requires-Dist: rich>=14.0.0
46
+ Requires-Dist: uvicorn>=0.33.0
47
+ Provides-Extra: dev
48
+ Requires-Dist: black; extra == 'dev'
49
+ Requires-Dist: mypy; extra == 'dev'
50
+ Requires-Dist: ruff; extra == 'dev'
51
+ Description-Content-Type: text/markdown
52
+
53
+ # Spaik SDK
54
+
55
+ Python SDK for building AI agents with multi-LLM support, streaming, and production infrastructure.
56
+
57
+ ## Installation
58
+
59
+ ```bash
60
+ pip install spaik-sdk
61
+ ```
62
+
63
+ ## Quick Start
64
+
65
+ ```python
66
+ from spaik_sdk.agent.base_agent import BaseAgent
67
+
68
+ class MyAgent(BaseAgent):
69
+ pass
70
+
71
+ agent = MyAgent(system_prompt="You are a helpful assistant.")
72
+ print(agent.get_response_text("Hello!"))
73
+ ```
74
+
75
+ ## Features
76
+
77
+ - **Multi-LLM Support**: OpenAI, Anthropic, Google, Azure, Ollama
78
+ - **Unified API**: Same interface across all providers
79
+ - **Streaming**: Real-time response streaming via SSE
80
+ - **Tools**: Function calling with LangChain integration
81
+ - **Structured Output**: Pydantic model responses
82
+ - **Server**: FastAPI with thread persistence, auth, file uploads
83
+ - **Audio**: Text-to-speech and speech-to-text
84
+ - **Cost Tracking**: Token usage and cost estimation
85
+
86
+ ## Agent API
87
+
88
+ ### Basic Response Methods
89
+
90
+ ```python
91
+ from spaik_sdk.agent.base_agent import BaseAgent
92
+ from spaik_sdk.models.model_registry import ModelRegistry
93
+
94
+ agent = MyAgent(
95
+ system_prompt="You are helpful.",
96
+ llm_model=ModelRegistry.CLAUDE_4_SONNET
97
+ )
98
+
99
+ # Sync - text only
100
+ text = agent.get_response_text("Hello")
101
+
102
+ # Sync - full message with blocks
103
+ message = agent.get_response("Hello")
104
+ print(message.get_text_content())
105
+
106
+ # Async
107
+ message = await agent.get_response_async("Hello")
108
+ ```
109
+
110
+ ### Streaming
111
+
112
+ ```python
113
+ # Token stream
114
+ async for chunk in agent.get_response_stream("Write a story"):
115
+ print(chunk, end="", flush=True)
116
+
117
+ # Event stream (for SSE)
118
+ async for event in agent.get_event_stream("Write a story"):
119
+ if event.get_event_type() == "StreamingUpdated":
120
+ print(event.content, end="")
121
+ ```
122
+
123
+ ### Structured Output
124
+
125
+ ```python
126
+ from pydantic import BaseModel
127
+
128
+ class Recipe(BaseModel):
129
+ name: str
130
+ ingredients: list[str]
131
+ steps: list[str]
132
+
133
+ recipe = agent.get_structured_response("Give me a pasta recipe", Recipe)
134
+ print(recipe.name)
135
+ ```
136
+
137
+ ### Interactive CLI
138
+
139
+ ```python
140
+ agent.run_cli() # Starts interactive chat in terminal
141
+ ```
142
+
143
+ ## Tools
144
+
145
+ ```python
146
+ from spaik_sdk.tools.tool_provider import ToolProvider, BaseTool, tool
147
+
148
+ class WeatherTools(ToolProvider):
149
+ def get_tools(self) -> list[BaseTool]:
150
+ @tool
151
+ def get_weather(city: str) -> str:
152
+ """Get current weather for a city."""
153
+ return f"Sunny, 22°C in {city}"
154
+
155
+ @tool
156
+ def get_forecast(city: str, days: int = 3) -> str:
157
+ """Get weather forecast."""
158
+ return f"{days}-day forecast for {city}: Sunny"
159
+
160
+ return [get_weather, get_forecast]
161
+
162
+ class WeatherAgent(BaseAgent):
163
+ def get_tool_providers(self) -> list[ToolProvider]:
164
+ return [WeatherTools()]
165
+
166
+ agent = WeatherAgent(system_prompt="You provide weather info.")
167
+ print(agent.get_response_text("What's the weather in Tokyo?"))
168
+ ```
169
+
170
+ ### Built-in Tool Providers
171
+
172
+ ```python
173
+ from spaik_sdk.tools.impl.search_tool_provider import SearchToolProvider
174
+ from spaik_sdk.tools.impl.mcp_tool_provider import MCPToolProvider
175
+
176
+ class MyAgent(BaseAgent):
177
+ def get_tool_providers(self):
178
+ return [
179
+ SearchToolProvider(), # Web search (Tavily)
180
+ MCPToolProvider(server), # MCP server tools
181
+ ]
182
+ ```
183
+
184
+ ## Models
185
+
186
+ ```python
187
+ from spaik_sdk.models.model_registry import ModelRegistry
188
+
189
+ # Anthropic
190
+ ModelRegistry.CLAUDE_4_SONNET
191
+ ModelRegistry.CLAUDE_4_OPUS
192
+ ModelRegistry.CLAUDE_4_5_SONNET
193
+ ModelRegistry.CLAUDE_4_5_OPUS
194
+
195
+ # OpenAI
196
+ ModelRegistry.GPT_4_1
197
+ ModelRegistry.GPT_4O
198
+ ModelRegistry.O4_MINI
199
+
200
+ # Google
201
+ ModelRegistry.GEMINI_2_5_FLASH
202
+ ModelRegistry.GEMINI_2_5_PRO
203
+
204
+ # Aliases
205
+ ModelRegistry.from_name("sonnet") # CLAUDE_4_SONNET
206
+ ModelRegistry.from_name("gpt 4.1") # GPT_4_1
207
+ ModelRegistry.from_name("gemini 2.5") # GEMINI_2_5_FLASH
208
+
209
+ # Custom model
210
+ from spaik_sdk.models.llm_model import LLMModel
211
+ from spaik_sdk.models.llm_families import LLMFamilies
212
+
213
+ custom = LLMModel(
214
+ family=LLMFamilies.OPENAI,
215
+ name="gpt-4-custom",
216
+ reasoning=False
217
+ )
218
+ ModelRegistry.register_custom(custom)
219
+ ```
220
+
221
+ ## FastAPI Server
222
+
223
+ ```python
224
+ from contextlib import asynccontextmanager
225
+ from fastapi import FastAPI
226
+ from fastapi.middleware.cors import CORSMiddleware
227
+ from spaik_sdk.agent.base_agent import BaseAgent
228
+ from spaik_sdk.server.api.routers.api_builder import ApiBuilder
229
+
230
+ class MyAgent(BaseAgent):
231
+ pass
232
+
233
+ @asynccontextmanager
234
+ async def lifespan(app: FastAPI):
235
+ agent = MyAgent(system_prompt="You are helpful.")
236
+ api_builder = ApiBuilder.local(agent=agent)
237
+
238
+ app.include_router(api_builder.build_thread_router())
239
+ app.include_router(api_builder.build_file_router())
240
+ app.include_router(api_builder.build_audio_router())
241
+ yield
242
+
243
+ app = FastAPI(lifespan=lifespan)
244
+ app.add_middleware(
245
+ CORSMiddleware,
246
+ allow_origins=["*"],
247
+ allow_methods=["*"],
248
+ allow_headers=["*"],
249
+ )
250
+ ```
251
+
252
+ ### API Endpoints
253
+
254
+ Thread management:
255
+ - `POST /threads` - Create thread
256
+ - `GET /threads` - List threads
257
+ - `GET /threads/{id}` - Get thread with messages
258
+ - `POST /threads/{id}/messages/stream` - Send message (SSE)
259
+ - `DELETE /threads/{id}` - Delete thread
260
+ - `POST /threads/{id}/cancel` - Cancel generation
261
+
262
+ Files:
263
+ - `POST /files` - Upload file
264
+ - `GET /files/{id}` - Download file
265
+
266
+ Audio:
267
+ - `POST /audio/speech` - Text to speech
268
+ - `POST /audio/transcribe` - Speech to text
269
+
270
+ ### Production Setup
271
+
272
+ ```python
273
+ from spaik_sdk.server.storage.impl.local_file_thread_repository import LocalFileThreadRepository
274
+ from spaik_sdk.server.authorization.base_authorizer import BaseAuthorizer
275
+
276
+ # Custom repository and auth
277
+ api_builder = ApiBuilder.stateful(
278
+ repository=LocalFileThreadRepository(base_path="./data"),
279
+ authorizer=MyAuthorizer(),
280
+ agent=agent,
281
+ )
282
+ ```
283
+
284
+ ## Orchestration
285
+
286
+ Code-first workflow orchestration without graph DSLs:
287
+
288
+ ```python
289
+ from spaik_sdk.orchestration import BaseOrchestrator, OrchestratorEvent
290
+ from dataclasses import dataclass
291
+ from typing import AsyncIterator
292
+
293
+ @dataclass
294
+ class State:
295
+ items: list[str]
296
+
297
+ @dataclass
298
+ class Result:
299
+ count: int
300
+
301
+ class MyOrchestrator(BaseOrchestrator[State, Result]):
302
+ async def run(self) -> AsyncIterator[OrchestratorEvent[Result]]:
303
+ state = State(items=[])
304
+
305
+ # Run step with automatic status events
306
+ async for event in self.step("fetch", "Fetching data", self.fetch, state):
307
+ yield event
308
+ if event.result:
309
+ state = event.result
310
+
311
+ # Progress updates
312
+ for i, item in enumerate(state.items):
313
+ yield self.progress("process", i + 1, len(state.items))
314
+ await self.process(item)
315
+
316
+ yield self.ok(Result(count=len(state.items)))
317
+
318
+ async def fetch(self, state: State) -> State:
319
+ return State(items=["a", "b", "c"])
320
+
321
+ async def process(self, item: str):
322
+ pass
323
+
324
+ # Run
325
+ orchestrator = MyOrchestrator()
326
+ result = orchestrator.run_sync()
327
+ ```
328
+
329
+ ## Configuration
330
+
331
+ Environment variables:
332
+
333
+ ```bash
334
+ # LLM Providers (at least one required)
335
+ ANTHROPIC_API_KEY=sk-ant-...
336
+ OPENAI_API_KEY=sk-...
337
+ GOOGLE_API_KEY=...
338
+
339
+ # Optional
340
+ AZURE_API_KEY=...
341
+ AZURE_ENDPOINT=https://your-resource.openai.azure.com/
342
+ DEFAULT_MODEL=claude-sonnet-4-20250514
343
+ ```
344
+
345
+ ## Development
346
+
347
+ ```bash
348
+ # Setup
349
+ uv sync
350
+
351
+ # Tests
352
+ make test # All
353
+ make test-unit # Unit only
354
+ make test-integration # Integration only
355
+ make test-unit-single PATTERN=name # Single test
356
+
357
+ # Quality
358
+ make lint # Check linting
359
+ make lint-fix # Fix linting
360
+ make typecheck # Type check
361
+ ```
362
+
363
+ ## Message Structure
364
+
365
+ Messages contain blocks of different types:
366
+
367
+ ```python
368
+ from spaik_sdk.thread.models import MessageBlockType
369
+
370
+ # Block types
371
+ MessageBlockType.PLAIN # Regular text
372
+ MessageBlockType.REASONING # Chain of thought
373
+ MessageBlockType.TOOL_USE # Tool call
374
+ MessageBlockType.ERROR # Error message
375
+ ```
376
+
377
+ ## License
378
+
379
+ MIT - Copyright (c) 2025 Siili Solutions Oyj