tinyflow-llm 0.1.0__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 (43) hide show
  1. tinyflow/__init__.py +53 -0
  2. tinyflow/config/helpers.py +62 -0
  3. tinyflow/config/settings.py +80 -0
  4. tinyflow/core/__init__.py +31 -0
  5. tinyflow/core/agent.py +457 -0
  6. tinyflow/core/exceptions.py +63 -0
  7. tinyflow/core/logger.py +13 -0
  8. tinyflow/core/message.py +6 -0
  9. tinyflow/core/protocol.py +81 -0
  10. tinyflow/core/tools.py +200 -0
  11. tinyflow/core/types.py +252 -0
  12. tinyflow/embeddings/__init__.py +4 -0
  13. tinyflow/embeddings/base.py +32 -0
  14. tinyflow/embeddings/factory.py +140 -0
  15. tinyflow/embeddings/local_embedding.py +65 -0
  16. tinyflow/embeddings/openai_embedding.py +70 -0
  17. tinyflow/memory/__init__.py +5 -0
  18. tinyflow/memory/base.py +16 -0
  19. tinyflow/memory/simple.py +34 -0
  20. tinyflow/memory/vector.py +26 -0
  21. tinyflow/providers/anthropic_llm.py +132 -0
  22. tinyflow/providers/base/factory.py +81 -0
  23. tinyflow/providers/base/llm.py +37 -0
  24. tinyflow/providers/gemini_llm.py +130 -0
  25. tinyflow/providers/openai_llm.py +198 -0
  26. tinyflow/tools/builtin/__init__.py +36 -0
  27. tinyflow/tools/builtin/code_execution.py +143 -0
  28. tinyflow/tools/builtin/search.py +145 -0
  29. tinyflow/tools/builtin/web_reader.py +88 -0
  30. tinyflow/vector/__init__.py +4 -0
  31. tinyflow/vector/base.py +65 -0
  32. tinyflow/vector/chroma_db.py +134 -0
  33. tinyflow/vector/factory.py +84 -0
  34. tinyflow/vector/qdrant_db.py +198 -0
  35. tinyflow/workflow/__init__.py +21 -0
  36. tinyflow/workflow/executor.py +272 -0
  37. tinyflow/workflow/hooks.py +191 -0
  38. tinyflow/workflow/state.py +148 -0
  39. tinyflow/workflow/step.py +74 -0
  40. tinyflow_llm-0.1.0.dist-info/METADATA +243 -0
  41. tinyflow_llm-0.1.0.dist-info/RECORD +43 -0
  42. tinyflow_llm-0.1.0.dist-info/WHEEL +4 -0
  43. tinyflow_llm-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,140 @@
1
+ """Embedding Factory
2
+
3
+ Select different Embedding implementations based on configuration
4
+
5
+ Supports:
6
+ - openai: OpenAI API Embedding
7
+ - local / sentence-transformers: Local Sentence Transformers model
8
+
9
+ Configuration precedence: Parameters > settings > Environment variables > Default values
10
+ """
11
+
12
+ from typing import Optional
13
+
14
+ from tinyflow.config.helpers import filter_none_kwargs, get_config_value
15
+ from tinyflow.config.settings import settings
16
+ from tinyflow.embeddings.base import BaseEmbedding
17
+ from tinyflow.embeddings.openai_embedding import OpenAIEmbedding
18
+
19
+
20
+ class EmbeddingFactory:
21
+ """Embedding Factory class.
22
+
23
+ Configuration Precedence:
24
+ 1. Explicit parameters (highest)
25
+ 2. Environment variables (EMBEDDING_PROVIDER)
26
+ 3. Settings (from .env or defaults)
27
+ 4. Provider defaults (lowest)
28
+ """
29
+
30
+ @staticmethod
31
+ def create(
32
+ provider: Optional[str] = None,
33
+ **kwargs,
34
+ ) -> BaseEmbedding:
35
+ """Create Embedding provider instance.
36
+
37
+ Args:
38
+ provider: Provider name ('openai', 'local').
39
+ Falls back to EMBEDDING_PROVIDER env var -> settings -> default.
40
+ **kwargs: Provider-specific configuration parameters.
41
+ - OpenAI: api_key, model, base_url
42
+ - Local: model_path, device
43
+
44
+ Returns:
45
+ Configured embedding provider instance.
46
+
47
+ Raises:
48
+ ValueError: If provider is not specified or supported.
49
+ """
50
+ provider = get_config_value(
51
+ provider,
52
+ env_key="EMBEDDING_PROVIDER",
53
+ settings_field="EMBEDDING_PROVIDER",
54
+ default_value="openai",
55
+ )
56
+
57
+ if not provider:
58
+ raise ValueError("Embedding Provider must be specified")
59
+
60
+ provider = provider.lower()
61
+
62
+ if provider == "openai":
63
+ openai_kwargs = {
64
+ "api_key": get_config_value(
65
+ kwargs.get("api_key"), "EMBEDDING_API_KEY", "EMBEDDING_API_KEY"
66
+ ),
67
+ "base_url": get_config_value(
68
+ kwargs.get("base_url"), "EMBEDDING_BASE_URL", "EMBEDDING_BASE_URL"
69
+ ),
70
+ "model": get_config_value(
71
+ kwargs.get("model"), "EMBEDDING_MODEL", "EMBEDDING_MODEL"
72
+ ),
73
+ }
74
+ # Use LLM_API_KEY as fallback if EMBEDDING_API_KEY is not set
75
+ if not openai_kwargs["api_key"]:
76
+ openai_kwargs["api_key"] = get_config_value(
77
+ None, "LLM_API_KEY", "LLM_API_KEY"
78
+ )
79
+
80
+ openai_kwargs = filter_none_kwargs(**openai_kwargs)
81
+ return OpenAIEmbedding(**openai_kwargs)
82
+
83
+ elif provider in ["local", "sentence-transformers"]:
84
+ # Lazy import to avoid hard dependency on torch/sentence_transformers
85
+ from tinyflow.embeddings.local_embedding import SentenceTransformerEmbedding
86
+
87
+ local_kwargs = {
88
+ "model_path": get_config_value(
89
+ kwargs.get("model_path"),
90
+ "EMBEDDING_MODEL_PATH",
91
+ "EMBEDDING_MODEL_PATH",
92
+ ),
93
+ "device": get_config_value(
94
+ kwargs.get("device"), "EMBEDDING_MODEL_DEVICE", "EMBEDDING_MODEL_DEVICE"
95
+ ),
96
+ }
97
+ local_kwargs = filter_none_kwargs(**local_kwargs)
98
+ return SentenceTransformerEmbedding(**local_kwargs)
99
+
100
+ else:
101
+ raise ValueError(
102
+ f"Unsupported embedding provider: {provider}. Supported: openai, local"
103
+ )
104
+
105
+ if not provider:
106
+ raise ValueError("Embedding Provider must be specified")
107
+
108
+ provider = provider.lower()
109
+
110
+ if provider == "openai":
111
+ openai_kwargs = {
112
+ "api_key": get_config_value(
113
+ kwargs.get("api_key"), "EMBEDDING_API_KEY", "EMBEDDING_API_KEY"
114
+ ),
115
+ "base_url": get_config_value(
116
+ kwargs.get("base_url"), "EMBEDDING_BASE_URL", "EMBEDDING_BASE_URL"
117
+ ),
118
+ "model": get_config_value(
119
+ kwargs.get("model"), "EMBEDDING_MODEL", "EMBEDDING_MODEL"
120
+ ),
121
+ }
122
+ openai_kwargs = filter_none_kwargs(**openai_kwargs)
123
+ return OpenAIEmbedding(**openai_kwargs)
124
+
125
+ elif provider in ("local", "sentence-transformers"):
126
+ local_kwargs = {
127
+ "model_path": get_config_value(
128
+ kwargs.get("model_path"), "LOCAL_MODEL", "EMBEDDING_MODEL_PATH"
129
+ ),
130
+ "device": get_config_value(
131
+ kwargs.get("device"), "DEVICE", "EMBEDDING_MODEL_DEVICE"
132
+ ),
133
+ }
134
+ local_kwargs = filter_none_kwargs(**local_kwargs)
135
+ return SentenceTransformerEmbedding(**local_kwargs)
136
+ else:
137
+ raise ValueError(
138
+ f"Unsupported embedding provider: {provider}. "
139
+ "Supported providers: openai, local, sentence-transformers"
140
+ )
@@ -0,0 +1,65 @@
1
+ """Sentence Transformers Local Embedding Implementation
2
+
3
+ Use open source models for local embedding
4
+
5
+ Note: This module requires torch and sentence_transformers.
6
+ If installation fails, use tinyflow.embeddings.openai_embedding.OpenAIEmbedding instead.
7
+ """
8
+
9
+ from typing import Any, List, Optional
10
+
11
+ from sentence_transformers import SentenceTransformer
12
+
13
+ from tinyflow.config.settings import settings
14
+ from tinyflow.embeddings.base import BaseEmbedding
15
+
16
+
17
+ class SentenceTransformerEmbedding(BaseEmbedding):
18
+ """Local Sentence Transformers Embedding Implementation
19
+
20
+ Supports:
21
+ - all-MiniLM-L6-v2 (384 dimensions)
22
+ - all-mpnet-base-v2 (768 dimensions)
23
+ - paraphrase-multilingual-MiniLM-L12-v2 (768 dimensions)
24
+
25
+ Configuration precedence: Parameters > settings > Environment variables > Default values
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ model_path: Optional[str] = None,
31
+ device: Optional[str] = None,
32
+ ):
33
+ """Initialize Sentence Transformer Embedding
34
+
35
+ Args:
36
+ model_path: Model path or name (defaults to settings or environment variables)
37
+ device: Computation device (auto/cpu/cuda/mps, defaults to settings or environment variables)
38
+
39
+ Raises:
40
+ ImportError: If torch/sentence_transformers is not installed
41
+ """
42
+ model_path = model_path or settings.EMBEDDING_MODEL_PATH
43
+ device = device or settings.EMBEDDING_MODEL_DEVICE
44
+
45
+ self.model = SentenceTransformer(model_path)
46
+ self.device = device
47
+
48
+ def embed(self, texts: List[str]) -> List[List[float]]:
49
+ """Convert text list to vectors"""
50
+ self.model.to(self.device)
51
+
52
+ embeddings: Any = self.model.encode(
53
+ texts,
54
+ convert_to_numpy=True,
55
+ show_progress_bar=False,
56
+ )
57
+
58
+ return list(embeddings)
59
+
60
+ def get_dimension(self) -> int:
61
+ """Get vector dimension"""
62
+ dim = self.model.get_sentence_embedding_dimension()
63
+ if dim is None:
64
+ raise ValueError("Unable to determine embedding dimension")
65
+ return dim
@@ -0,0 +1,70 @@
1
+ """OpenAI Embedding Implementation
2
+
3
+ Use OpenAI API for text embedding
4
+ """
5
+
6
+ from typing import List, Optional
7
+
8
+ from openai import AsyncOpenAI
9
+
10
+ from tinyflow.config.settings import settings
11
+ from tinyflow.embeddings.base import BaseEmbedding
12
+
13
+
14
+ class OpenAIEmbedding(BaseEmbedding):
15
+ """OpenAI Embedding Implementation
16
+
17
+ Supports:
18
+ - text-embedding-3-small
19
+ - text-embedding-3-large
20
+ - text-embedding-ada-002
21
+
22
+ Configuration precedence: Parameters > settings > Environment variables > Default values
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ api_key: Optional[str] = None,
28
+ base_url: Optional[str] = None,
29
+ model: Optional[str] = None,
30
+ ):
31
+ """Initialize OpenAI Embedding
32
+
33
+ Args:
34
+ api_key: OpenAI API key (defaults to settings or environment variables)
35
+ base_url: API base URL (defaults to settings or environment variables)
36
+ model: Model name (defaults to settings or environment variables)
37
+ """
38
+ api_key = api_key or settings.EMBEDDING_API_KEY or settings.LLM_API_KEY or ""
39
+ base_url = base_url or settings.EMBEDDING_BASE_URL or settings.LLM_BASE_URL
40
+ model = model or settings.EMBEDDING_MODEL
41
+
42
+ self.client = AsyncOpenAI(api_key=api_key, base_url=base_url)
43
+ self.model = model
44
+
45
+ async def embed(self, texts: List[str]) -> List[List[float]]:
46
+ """Convert text list to vectors"""
47
+ # Batch processing, max 2048 texts per batch
48
+ embeddings = []
49
+ batch_size = 2048
50
+
51
+ for i in range(0, len(texts), batch_size):
52
+ batch = texts[i : i + batch_size]
53
+ response = await self.client.embeddings.create(
54
+ input=batch,
55
+ model=self.model,
56
+ )
57
+
58
+ batch_embeddings = [item.embedding for item in response.data]
59
+ embeddings.extend(batch_embeddings)
60
+
61
+ return embeddings
62
+
63
+ def get_dimension(self) -> int:
64
+ """Get vector dimension"""
65
+ model_dimensions = {
66
+ "text-embedding-3-small": 1536,
67
+ "text-embedding-3-large": 3072,
68
+ "text-embedding-ada-002": 1536,
69
+ }
70
+ return model_dimensions.get(self.model, 1536)
@@ -0,0 +1,5 @@
1
+ from .base import BaseMemory
2
+ from .simple import SimpleMemory
3
+ from .vector import VectorMemory
4
+
5
+ __all__ = ["BaseMemory", "SimpleMemory", "VectorMemory"]
@@ -0,0 +1,16 @@
1
+ import abc
2
+ from typing import List
3
+
4
+
5
+ class BaseMemory(abc.ABC):
6
+ @abc.abstractmethod
7
+ async def add(self, text: str):
8
+ pass
9
+
10
+ @abc.abstractmethod
11
+ async def search(self, query: str, limit: int = 3) -> List[str]:
12
+ pass
13
+
14
+ @abc.abstractmethod
15
+ async def clear(self):
16
+ pass
@@ -0,0 +1,34 @@
1
+ from typing import List
2
+
3
+ from .base import BaseMemory
4
+
5
+
6
+ class SimpleMemory(BaseMemory):
7
+ def __init__(self):
8
+ self.memories: List[str] = []
9
+
10
+ async def add(self, text: str):
11
+ if text not in self.memories:
12
+ self.memories.append(text)
13
+
14
+ async def search(self, query: str, limit: int = 3) -> List[str]:
15
+ if not self.memories:
16
+ return []
17
+
18
+ if len(self.memories) <= limit:
19
+ return self.memories
20
+
21
+ query_words = set(query.lower().split())
22
+
23
+ scored_memories = []
24
+ for i, text in enumerate(self.memories):
25
+ text_words = set(text.lower().split())
26
+ score = len(query_words.intersection(text_words))
27
+ score += i / 1000000
28
+ scored_memories.append((score, text))
29
+
30
+ scored_memories.sort(key=lambda x: x[0], reverse=True)
31
+ return [text for score, text in scored_memories[:limit]]
32
+
33
+ async def clear(self):
34
+ self.memories = []
@@ -0,0 +1,26 @@
1
+ import logging
2
+ from typing import List
3
+
4
+ from tinyflow.vector.base import BaseVectorDB
5
+
6
+ from .base import BaseMemory
7
+
8
+ logger = logging.getLogger("tinyflow.memory.vector")
9
+
10
+
11
+ class VectorMemory(BaseMemory):
12
+ def __init__(self, vector_db: BaseVectorDB):
13
+ self.db = vector_db
14
+
15
+ async def add(self, text: str):
16
+ logger.debug(f"Adding to vector memory: {text[:50]}...")
17
+ await self.db.add([text])
18
+
19
+ async def search(self, query: str, limit: int = 3) -> List[str]:
20
+ logger.debug(f"Searching vector memory for: {query}")
21
+ results = await self.db.search(query, limit=limit)
22
+ return [r["text"] for r in results]
23
+
24
+ async def clear(self):
25
+ logger.info("Clearing vector memory")
26
+ await self.db.clear()
@@ -0,0 +1,132 @@
1
+ import logging
2
+ import os
3
+ from typing import AsyncGenerator, List, Optional
4
+
5
+ import anthropic
6
+ from tenacity import retry, stop_after_attempt, wait_exponential
7
+
8
+ from tinyflow.core.tools import Tool
9
+ from tinyflow.core.types import (
10
+ LLMResponse,
11
+ Message,
12
+ StreamChunk,
13
+ StreamPart,
14
+ TextStreamDelta,
15
+ )
16
+ from tinyflow.providers.base.llm import BaseLLM
17
+
18
+ logger = logging.getLogger("tinyflow.providers.anthropic")
19
+
20
+
21
+ class AnthropicProvider(BaseLLM):
22
+ def __init__(
23
+ self,
24
+ base_url: Optional[str] = None,
25
+ api_key: Optional[str] = None,
26
+ model: Optional[str] = None,
27
+ timeout: float = 60.0,
28
+ ):
29
+ # Configuration precedence: Manual > Environment Variable > Error
30
+ self.api_key = api_key or os.getenv("ANTHROPIC_API_KEY") or os.getenv("LLM_API_KEY")
31
+ self.model = (
32
+ model
33
+ or os.getenv("ANTHROPIC_MODEL")
34
+ or os.getenv("LLM_MODEL")
35
+ or "claude-3-opus-20240229"
36
+ )
37
+ self.timeout = timeout
38
+
39
+ if not self.api_key:
40
+ raise ValueError(
41
+ "Anthropic API key required. Set ANTHROPIC_API_KEY env var or pass api_key."
42
+ )
43
+
44
+ self.client = anthropic.AsyncAnthropic(
45
+ base_url=base_url,
46
+ api_key=self.api_key,
47
+ timeout=self.timeout,
48
+ )
49
+
50
+ @retry(
51
+ wait=wait_exponential(multiplier=1, min=2, max=10),
52
+ stop=stop_after_attempt(3),
53
+ reraise=True,
54
+ )
55
+ async def generate(
56
+ self, messages: List[Message], tools: Optional[List[Tool]] = None
57
+ ) -> LLMResponse:
58
+ logger.debug(f"Anthropic Generate call with {len(messages)} messages")
59
+ system_prompt = ""
60
+ claude_messages = []
61
+
62
+ for m in messages:
63
+ if m.role == "system":
64
+ system_prompt += (m.content or "") + "\n"
65
+ else:
66
+ claude_messages.append({"role": m.role, "content": m.content})
67
+
68
+ response = await self.client.messages.create(
69
+ model=self.model,
70
+ max_tokens=1024,
71
+ system=system_prompt,
72
+ messages=claude_messages,
73
+ )
74
+
75
+ content_text = ""
76
+ for block in response.content:
77
+ if isinstance(block, anthropic.types.TextBlock):
78
+ content_text += block.text
79
+
80
+ logger.debug(f"Anthropic Generate response: {content_text[:50]}...")
81
+
82
+ return LLMResponse(content=content_text, raw_response=response)
83
+
84
+ def stream(
85
+ self, messages: List[Message], tools: Optional[List[Tool]] = None
86
+ ) -> AsyncGenerator[StreamChunk, None]:
87
+ system_prompt = ""
88
+ claude_messages = []
89
+
90
+ for m in messages:
91
+ if m.role == "system":
92
+ system_prompt += (m.content or "") + "\n"
93
+ else:
94
+ claude_messages.append({"role": m.role, "content": m.content})
95
+
96
+ async def _stream():
97
+ async with self.client.messages.stream(
98
+ model=self.model,
99
+ max_tokens=1024,
100
+ system=system_prompt,
101
+ messages=claude_messages,
102
+ ) as stream:
103
+ async for text in stream.text_stream:
104
+ if text:
105
+ yield StreamChunk(content=text)
106
+
107
+ return _stream()
108
+
109
+ def stream_text(
110
+ self, messages: List[Message], tools: Optional[List[Tool]] = None
111
+ ) -> AsyncGenerator[StreamPart, None]:
112
+ system_prompt = ""
113
+ claude_messages = []
114
+
115
+ for m in messages:
116
+ if m.role == "system":
117
+ system_prompt += (m.content or "") + "\n"
118
+ else:
119
+ claude_messages.append({"role": m.role, "content": m.content})
120
+
121
+ async def _stream_text():
122
+ async with self.client.messages.stream(
123
+ model=self.model,
124
+ max_tokens=1024,
125
+ system=system_prompt,
126
+ messages=claude_messages,
127
+ ) as stream:
128
+ async for text in stream.text_stream:
129
+ if text:
130
+ yield TextStreamDelta(type="text", text=text)
131
+
132
+ return _stream_text()
@@ -0,0 +1,81 @@
1
+ import logging
2
+ from typing import Optional
3
+
4
+ from tinyflow.config.helpers import filter_none_kwargs, get_config_value
5
+ from tinyflow.core.exceptions import ConfigurationError
6
+ from tinyflow.providers.anthropic_llm import AnthropicProvider
7
+ from tinyflow.providers.base.llm import BaseLLM
8
+ from tinyflow.providers.gemini_llm import GeminiProvider
9
+ from tinyflow.providers.openai_llm import OpenAIProvider
10
+
11
+ logger = logging.getLogger("tinyflow.providers.factory")
12
+
13
+
14
+ class LLMFactory:
15
+ """Factory for creating LLM provider instances.
16
+
17
+ Supports both configuration-driven (env vars) and explicit instantiation.
18
+ Configuration Precedence:
19
+ 1. Explicit parameters (highest)
20
+ 2. Environment variables (LLM_*)
21
+ 3. Settings (from .env or defaults)
22
+ 4. Provider defaults (lowest)
23
+ """
24
+
25
+ @staticmethod
26
+ def create(
27
+ provider: Optional[str] = None,
28
+ **kwargs,
29
+ ) -> BaseLLM:
30
+ """Create an LLM provider instance.
31
+
32
+ Args:
33
+ provider: Provider name ('openai', 'anthropic', 'gemini').
34
+ Falls back to LLM_PROVIDER env var -> settings -> default.
35
+ **kwargs: Additional provider-specific arguments (api_key, model, base_url, etc.)
36
+ Common args (model, api_key, base_url) are resolved against
37
+ LLM_* env vars and settings if not provided.
38
+
39
+ Returns:
40
+ Configured provider instance.
41
+
42
+ Raises:
43
+ ValueError: If provider is not specified and not found in configuration.
44
+ """
45
+ # Determine provider
46
+ provider = get_config_value(
47
+ provider,
48
+ env_key="LLM_PROVIDER",
49
+ settings_field="LLM_PROVIDER",
50
+ default_value="openai",
51
+ )
52
+
53
+ if not provider:
54
+ raise ConfigurationError("LLM Provider must be specified")
55
+
56
+ provider = provider.lower()
57
+ logger.info(f"Creating LLM provider: {provider}")
58
+
59
+ common_kwargs = {
60
+ "base_url": get_config_value(kwargs.get("base_url"), "LLM_BASE_URL", "LLM_BASE_URL"),
61
+ "api_key": get_config_value(kwargs.get("api_key"), "LLM_API_KEY", "LLM_API_KEY"),
62
+ "model": get_config_value(kwargs.get("model"), "LLM_MODEL", "LLM_MODEL"),
63
+ }
64
+
65
+ provider_kwargs = {**kwargs, **common_kwargs}
66
+ provider_kwargs = filter_none_kwargs(**provider_kwargs)
67
+
68
+ if provider == "openai":
69
+ return OpenAIProvider(**provider_kwargs)
70
+ elif provider == "deepseek":
71
+ if "base_url" not in provider_kwargs:
72
+ provider_kwargs["base_url"] = "https://api.deepseek.com"
73
+ return OpenAIProvider(**provider_kwargs)
74
+ elif provider == "anthropic":
75
+ return AnthropicProvider(**provider_kwargs)
76
+ elif provider == "gemini":
77
+ return GeminiProvider(**provider_kwargs)
78
+ else:
79
+ raise ConfigurationError(
80
+ f"Unsupported provider: {provider}. Supported: openai, anthropic, gemini"
81
+ )
@@ -0,0 +1,37 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import AsyncGenerator, List, Optional
3
+
4
+ from tinyflow.core.tools import Tool
5
+ from tinyflow.core.types import LLMResponse, Message, StreamChunk, StreamPart
6
+
7
+
8
+ class BaseLLM(ABC):
9
+ @abstractmethod
10
+ async def generate(
11
+ self, messages: List[Message], tools: Optional[List[Tool]] = None
12
+ ) -> LLMResponse:
13
+ """Generate a complete response from the LLM."""
14
+ pass
15
+
16
+ @abstractmethod
17
+ def stream(
18
+ self, messages: List[Message], tools: Optional[List[Tool]] = None
19
+ ) -> AsyncGenerator[StreamChunk, None]:
20
+ """Stream response chunks (legacy method - use stream_text for rich types)."""
21
+ pass
22
+
23
+ @abstractmethod
24
+ def stream_text(
25
+ self, messages: List[Message], tools: Optional[List[Tool]] = None
26
+ ) -> AsyncGenerator[StreamPart, None]:
27
+ """Stream response with rich part types (text, reasoning, tool calls).
28
+
29
+ Yields StreamPart objects that can be:
30
+ - TextStreamDelta: Text content delta
31
+ - ReasoningStreamDelta: Reasoning/thinking trace
32
+ - ToolCallStreamStart: Tool call streaming begins
33
+ - ToolCallStreamDelta: Tool call argument delta
34
+ - ToolCallComplete: Complete tool call
35
+ - ToolResultStream: Tool execution result
36
+ """
37
+ pass