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.
- tinyflow/__init__.py +53 -0
- tinyflow/config/helpers.py +62 -0
- tinyflow/config/settings.py +80 -0
- tinyflow/core/__init__.py +31 -0
- tinyflow/core/agent.py +457 -0
- tinyflow/core/exceptions.py +63 -0
- tinyflow/core/logger.py +13 -0
- tinyflow/core/message.py +6 -0
- tinyflow/core/protocol.py +81 -0
- tinyflow/core/tools.py +200 -0
- tinyflow/core/types.py +252 -0
- tinyflow/embeddings/__init__.py +4 -0
- tinyflow/embeddings/base.py +32 -0
- tinyflow/embeddings/factory.py +140 -0
- tinyflow/embeddings/local_embedding.py +65 -0
- tinyflow/embeddings/openai_embedding.py +70 -0
- tinyflow/memory/__init__.py +5 -0
- tinyflow/memory/base.py +16 -0
- tinyflow/memory/simple.py +34 -0
- tinyflow/memory/vector.py +26 -0
- tinyflow/providers/anthropic_llm.py +132 -0
- tinyflow/providers/base/factory.py +81 -0
- tinyflow/providers/base/llm.py +37 -0
- tinyflow/providers/gemini_llm.py +130 -0
- tinyflow/providers/openai_llm.py +198 -0
- tinyflow/tools/builtin/__init__.py +36 -0
- tinyflow/tools/builtin/code_execution.py +143 -0
- tinyflow/tools/builtin/search.py +145 -0
- tinyflow/tools/builtin/web_reader.py +88 -0
- tinyflow/vector/__init__.py +4 -0
- tinyflow/vector/base.py +65 -0
- tinyflow/vector/chroma_db.py +134 -0
- tinyflow/vector/factory.py +84 -0
- tinyflow/vector/qdrant_db.py +198 -0
- tinyflow/workflow/__init__.py +21 -0
- tinyflow/workflow/executor.py +272 -0
- tinyflow/workflow/hooks.py +191 -0
- tinyflow/workflow/state.py +148 -0
- tinyflow/workflow/step.py +74 -0
- tinyflow_llm-0.1.0.dist-info/METADATA +243 -0
- tinyflow_llm-0.1.0.dist-info/RECORD +43 -0
- tinyflow_llm-0.1.0.dist-info/WHEEL +4 -0
- 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)
|
tinyflow/memory/base.py
ADDED
|
@@ -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
|