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,130 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import AsyncGenerator, List, Optional
|
|
3
|
+
|
|
4
|
+
from google import genai
|
|
5
|
+
from google.genai import types
|
|
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
|
+
|
|
19
|
+
class GeminiProvider(BaseLLM):
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
api_key: Optional[str] = None,
|
|
23
|
+
model: Optional[str] = None,
|
|
24
|
+
base_url: Optional[str] = None,
|
|
25
|
+
):
|
|
26
|
+
# Configuration precedence: Manual > Environment Variable > Error
|
|
27
|
+
self.api_key = api_key or os.getenv("GEMINI_API_KEY") or os.getenv("LLM_API_KEY")
|
|
28
|
+
self.model = (
|
|
29
|
+
model or os.getenv("GEMINI_MODEL") or os.getenv("LLM_MODEL") or "gemini-1.5-flash"
|
|
30
|
+
)
|
|
31
|
+
self.base_url = base_url # Gemini supports custom endpoints via client options
|
|
32
|
+
|
|
33
|
+
if not self.api_key:
|
|
34
|
+
raise ValueError(
|
|
35
|
+
"Gemini API key required. Set GEMINI_API_KEY env var or pass api_key."
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
self.client = genai.Client(
|
|
39
|
+
api_key=self.api_key,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def _convert_messages(self, messages: List[Message]) -> List[types.Content]:
|
|
43
|
+
"""
|
|
44
|
+
Gemini requires converting OpenAI-style messages to Google Content type
|
|
45
|
+
Note: This is a simplified conversion; complex Tool Results require more refined handling
|
|
46
|
+
"""
|
|
47
|
+
gemini_contents = []
|
|
48
|
+
for m in messages:
|
|
49
|
+
role = "user" if m.role in ["user", "tool"] else "model"
|
|
50
|
+
if m.role == "system":
|
|
51
|
+
# Gemini V1 SDK suggests passing system prompt via config's system_instruction
|
|
52
|
+
# Temporarily doing simple merging here, or extracting separately during generation
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
parts = [types.Part(text=m.content)] if m.content else []
|
|
56
|
+
|
|
57
|
+
# Simple conversion logic; production needs to handle function_call conversion
|
|
58
|
+
gemini_contents.append(types.Content(role=role, parts=parts))
|
|
59
|
+
|
|
60
|
+
return gemini_contents
|
|
61
|
+
|
|
62
|
+
def _get_system_prompt(self, messages: List[Message]) -> Optional[str]:
|
|
63
|
+
for m in messages:
|
|
64
|
+
if m.role == "system":
|
|
65
|
+
return m.content
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
@retry(
|
|
69
|
+
wait=wait_exponential(multiplier=1, min=2, max=10),
|
|
70
|
+
stop=stop_after_attempt(3),
|
|
71
|
+
reraise=True,
|
|
72
|
+
)
|
|
73
|
+
async def generate(
|
|
74
|
+
self, messages: List[Message], tools: Optional[List[Tool]] = None
|
|
75
|
+
) -> LLMResponse:
|
|
76
|
+
contents = self._convert_messages(messages)
|
|
77
|
+
system_instr = self._get_system_prompt(messages)
|
|
78
|
+
|
|
79
|
+
# Gemini supports passing functions directly, SDK automatically parses Schema
|
|
80
|
+
# But we already have Tool; for simplicity, momentarily disabling Tools for Gemini
|
|
81
|
+
if tools:
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
response = await self.client.aio.models.generate_content(
|
|
85
|
+
model=self.model,
|
|
86
|
+
contents=contents,
|
|
87
|
+
config=types.GenerateContentConfig(system_instruction=system_instr),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
return LLMResponse(content=response.text, raw_response=response)
|
|
91
|
+
|
|
92
|
+
def stream(
|
|
93
|
+
self, messages: List[Message], tools: Optional[List[Tool]] = None
|
|
94
|
+
) -> AsyncGenerator[StreamChunk, None]:
|
|
95
|
+
contents = self._convert_messages(messages)
|
|
96
|
+
system_instr = self._get_system_prompt(messages)
|
|
97
|
+
|
|
98
|
+
async def _stream():
|
|
99
|
+
# Call streaming interface
|
|
100
|
+
response_stream = await self.client.aio.models.generate_content_stream(
|
|
101
|
+
model=self.model,
|
|
102
|
+
contents=contents,
|
|
103
|
+
config=types.GenerateContentConfig(system_instruction=system_instr),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
async for chunk in response_stream:
|
|
107
|
+
if chunk.text:
|
|
108
|
+
yield StreamChunk(content=chunk.text)
|
|
109
|
+
|
|
110
|
+
return _stream()
|
|
111
|
+
|
|
112
|
+
def stream_text(
|
|
113
|
+
self, messages: List[Message], tools: Optional[List[Tool]] = None
|
|
114
|
+
) -> AsyncGenerator[StreamPart, None]:
|
|
115
|
+
"""Stream response with rich part types."""
|
|
116
|
+
contents = self._convert_messages(messages)
|
|
117
|
+
system_instr = self._get_system_prompt(messages)
|
|
118
|
+
|
|
119
|
+
async def _stream_text():
|
|
120
|
+
response_stream = await self.client.aio.models.generate_content_stream(
|
|
121
|
+
model=self.model,
|
|
122
|
+
contents=contents,
|
|
123
|
+
config=types.GenerateContentConfig(system_instruction=system_instr),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
async for chunk in response_stream:
|
|
127
|
+
if chunk.text:
|
|
128
|
+
yield TextStreamDelta(type="text", text=chunk.text)
|
|
129
|
+
|
|
130
|
+
return _stream_text()
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from typing import AsyncGenerator, List, Optional
|
|
4
|
+
|
|
5
|
+
from openai import AsyncOpenAI
|
|
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
|
+
ReasoningStreamDelta,
|
|
13
|
+
StreamChunk,
|
|
14
|
+
StreamPart,
|
|
15
|
+
TextStreamDelta,
|
|
16
|
+
ToolCallComplete,
|
|
17
|
+
ToolCallStreamDelta,
|
|
18
|
+
ToolCallStreamStart,
|
|
19
|
+
)
|
|
20
|
+
from tinyflow.providers.base.llm import BaseLLM
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger("tinyflow.providers.openai")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class OpenAIProvider(BaseLLM):
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
api_key: Optional[str] = None,
|
|
29
|
+
model: Optional[str] = None,
|
|
30
|
+
base_url: Optional[str] = None,
|
|
31
|
+
timeout: float = 60.0,
|
|
32
|
+
):
|
|
33
|
+
# Configuration precedence: Manual > Environment Variable > Error
|
|
34
|
+
self.api_key = api_key or os.getenv("OPENAI_API_KEY") or os.getenv("LLM_API_KEY")
|
|
35
|
+
self.model = model or os.getenv("OPENAI_MODEL") or os.getenv("LLM_MODEL") or "gpt-4o"
|
|
36
|
+
self.base_url = base_url or os.getenv("OPENAI_BASE_URL") or os.getenv("LLM_BASE_URL")
|
|
37
|
+
self.timeout = timeout
|
|
38
|
+
|
|
39
|
+
logging.info(
|
|
40
|
+
f"OpenAIProvider Initialized: model='{self.model}', base_url='{self.base_url}'"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if not self.api_key:
|
|
44
|
+
raise ValueError(
|
|
45
|
+
"OpenAI API key required. Set OPENAI_API_KEY env var or pass api_key."
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
self.client = AsyncOpenAI(
|
|
49
|
+
api_key=self.api_key,
|
|
50
|
+
base_url=self.base_url,
|
|
51
|
+
timeout=self.timeout,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def _convert_messages(self, messages: List[Message]):
|
|
55
|
+
openai_msgs = []
|
|
56
|
+
for m in messages:
|
|
57
|
+
msg = {"role": m.role, "content": m.content}
|
|
58
|
+
if m.tool_calls:
|
|
59
|
+
msg["tool_calls"] = m.tool_calls
|
|
60
|
+
if m.tool_call_id:
|
|
61
|
+
msg["tool_call_id"] = m.tool_call_id
|
|
62
|
+
if m.name:
|
|
63
|
+
msg["name"] = m.name
|
|
64
|
+
|
|
65
|
+
if m.reasoning_content:
|
|
66
|
+
msg["reasoning_content"] = m.reasoning_content
|
|
67
|
+
|
|
68
|
+
openai_msgs.append(msg)
|
|
69
|
+
return openai_msgs
|
|
70
|
+
|
|
71
|
+
@retry(
|
|
72
|
+
wait=wait_exponential(multiplier=1, min=2, max=10),
|
|
73
|
+
stop=stop_after_attempt(3),
|
|
74
|
+
reraise=True,
|
|
75
|
+
)
|
|
76
|
+
async def generate(
|
|
77
|
+
self, messages: List[Message], tools: Optional[List[Tool]] = None
|
|
78
|
+
) -> LLMResponse:
|
|
79
|
+
logger.debug(f"OpenAI Generate call with {len(messages)} messages")
|
|
80
|
+
openai_messages = self._convert_messages(messages)
|
|
81
|
+
openai_tools = [t.to_openai_schema(strict=True) for t in tools] if tools else None
|
|
82
|
+
|
|
83
|
+
response = await self.client.chat.completions.create(
|
|
84
|
+
model=self.model,
|
|
85
|
+
messages=openai_messages,
|
|
86
|
+
tools=openai_tools, # pyright: ignore[reportArgumentType]
|
|
87
|
+
)
|
|
88
|
+
msg = response.choices[0].message
|
|
89
|
+
logger.debug(
|
|
90
|
+
f"OpenAI Generate response: {msg.content[:50] if msg.content else 'None'}..."
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Process tool_calls format
|
|
94
|
+
tool_calls_dict = None
|
|
95
|
+
if msg.tool_calls:
|
|
96
|
+
tool_calls_dict = [
|
|
97
|
+
{
|
|
98
|
+
"id": tc.id,
|
|
99
|
+
"function": {
|
|
100
|
+
"name": tc.function.name,
|
|
101
|
+
"arguments": tc.function.arguments,
|
|
102
|
+
},
|
|
103
|
+
"type": "function",
|
|
104
|
+
}
|
|
105
|
+
for tc in msg.tool_calls
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
return LLMResponse(content=msg.content, tool_calls=tool_calls_dict, raw_response=response)
|
|
109
|
+
|
|
110
|
+
def stream(
|
|
111
|
+
self, messages: List[Message], tools: Optional[List[Tool]] = None
|
|
112
|
+
) -> AsyncGenerator[StreamChunk, None]:
|
|
113
|
+
openai_messages = self._convert_messages(messages)
|
|
114
|
+
|
|
115
|
+
async def _stream():
|
|
116
|
+
stream = await self.client.chat.completions.create(
|
|
117
|
+
model=self.model, messages=openai_messages, stream=True
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
async for chunk in stream:
|
|
121
|
+
if not chunk.choices:
|
|
122
|
+
continue
|
|
123
|
+
delta = chunk.choices[0].delta
|
|
124
|
+
if delta.content:
|
|
125
|
+
yield StreamChunk(content=delta.content)
|
|
126
|
+
|
|
127
|
+
return _stream()
|
|
128
|
+
|
|
129
|
+
def stream_text(
|
|
130
|
+
self, messages: List[Message], tools: Optional[List[Tool]] = None
|
|
131
|
+
) -> AsyncGenerator[StreamPart, None]:
|
|
132
|
+
openai_messages = self._convert_messages(messages)
|
|
133
|
+
openai_tools = [t.to_openai_schema(strict=True) for t in tools] if tools else None
|
|
134
|
+
|
|
135
|
+
async def _stream_text():
|
|
136
|
+
stream = await self.client.chat.completions.create(
|
|
137
|
+
model=self.model,
|
|
138
|
+
messages=openai_messages,
|
|
139
|
+
tools=openai_tools, # type: ignore[reportCallIssue]
|
|
140
|
+
stream=True,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
current_tool_call: Optional[dict] = None
|
|
144
|
+
|
|
145
|
+
async for chunk in stream:
|
|
146
|
+
if not chunk.choices:
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
delta = chunk.choices[0].delta
|
|
150
|
+
|
|
151
|
+
if delta.content:
|
|
152
|
+
yield TextStreamDelta(type="text", text=delta.content)
|
|
153
|
+
|
|
154
|
+
# Support DeepSeek/o1 "reasoning_content"
|
|
155
|
+
if hasattr(delta, "reasoning_content") and delta.reasoning_content:
|
|
156
|
+
yield ReasoningStreamDelta(type="reasoning", text=delta.reasoning_content)
|
|
157
|
+
|
|
158
|
+
if delta.tool_calls:
|
|
159
|
+
for tc_delta in delta.tool_calls:
|
|
160
|
+
if tc_delta.id:
|
|
161
|
+
current_tool_call = {
|
|
162
|
+
"id": tc_delta.id,
|
|
163
|
+
"name": tc_delta.function.name,
|
|
164
|
+
"args": tc_delta.function.arguments or "",
|
|
165
|
+
}
|
|
166
|
+
yield ToolCallStreamStart(
|
|
167
|
+
type="tool-call-streaming-start",
|
|
168
|
+
tool_call_id=current_tool_call["id"],
|
|
169
|
+
tool_name=current_tool_call["name"],
|
|
170
|
+
)
|
|
171
|
+
else:
|
|
172
|
+
if current_tool_call:
|
|
173
|
+
current_tool_call["args"] += tc_delta.function.arguments or ""
|
|
174
|
+
yield ToolCallStreamDelta(
|
|
175
|
+
type="tool-call-delta",
|
|
176
|
+
tool_call_id=current_tool_call["id"],
|
|
177
|
+
tool_name=current_tool_call["name"],
|
|
178
|
+
args_text_delta=tc_delta.function.arguments or "",
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
if chunk.choices[0].finish_reason:
|
|
182
|
+
if current_tool_call:
|
|
183
|
+
import json
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
args = json.loads(current_tool_call["args"])
|
|
187
|
+
except json.JSONDecodeError:
|
|
188
|
+
args = current_tool_call["args"]
|
|
189
|
+
|
|
190
|
+
yield ToolCallComplete(
|
|
191
|
+
type="tool-call",
|
|
192
|
+
tool_call_id=current_tool_call["id"],
|
|
193
|
+
tool_name=current_tool_call["name"],
|
|
194
|
+
input=args,
|
|
195
|
+
)
|
|
196
|
+
current_tool_call = None
|
|
197
|
+
|
|
198
|
+
return _stream_text()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Built-in tools for TinyFlow Agent framework.
|
|
2
|
+
|
|
3
|
+
This package provides ready-to-use tools for common Agent tasks:
|
|
4
|
+
- Search: Tavily AI and DuckDuckGo
|
|
5
|
+
- Web Reading: Jina AI Reader
|
|
6
|
+
- Code Execution: Python Interpreter (restricted)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from tinyflow.tools.builtin.code_execution import (
|
|
10
|
+
create_python_interpreter_tool,
|
|
11
|
+
python_interpreter_tool,
|
|
12
|
+
)
|
|
13
|
+
from tinyflow.tools.builtin.search import (
|
|
14
|
+
create_duckduckgo_search_tool,
|
|
15
|
+
create_tavily_search_tool,
|
|
16
|
+
duckduckgo_search_tool,
|
|
17
|
+
tavily_search_tool,
|
|
18
|
+
)
|
|
19
|
+
from tinyflow.tools.builtin.web_reader import (
|
|
20
|
+
create_jina_reader_tool,
|
|
21
|
+
jina_reader_tool,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
# Search tools
|
|
26
|
+
"create_tavily_search_tool",
|
|
27
|
+
"create_duckduckgo_search_tool",
|
|
28
|
+
"tavily_search_tool",
|
|
29
|
+
"duckduckgo_search_tool",
|
|
30
|
+
# Web reader tools
|
|
31
|
+
"create_jina_reader_tool",
|
|
32
|
+
"jina_reader_tool",
|
|
33
|
+
# Code execution
|
|
34
|
+
"create_python_interpreter_tool",
|
|
35
|
+
"python_interpreter_tool",
|
|
36
|
+
]
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Python interpreter tool for code execution.
|
|
2
|
+
|
|
3
|
+
⚠️ SECURITY WARNING: This tool executes arbitrary Python code.
|
|
4
|
+
Use only in sandboxed environments. Never expose to untrusted users.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import io
|
|
8
|
+
import sys
|
|
9
|
+
import traceback
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, Field
|
|
13
|
+
|
|
14
|
+
from tinyflow.core.tools import Tool
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PythonInterpreterInput(BaseModel):
|
|
18
|
+
"""Input parameters for Python interpreter."""
|
|
19
|
+
|
|
20
|
+
code: str = Field(
|
|
21
|
+
description="The Python code to execute. "
|
|
22
|
+
"Use print() to output results. "
|
|
23
|
+
"Available libraries: os, sys, math, json, re, datetime, collections, itertools"
|
|
24
|
+
)
|
|
25
|
+
timeout: int = Field(default=30, description="Maximum execution time in seconds (1-60)")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def create_python_interpreter_tool(
|
|
29
|
+
allowed_modules: Optional[list] = None, allow_file_access: bool = False
|
|
30
|
+
) -> Tool:
|
|
31
|
+
"""Create a Python interpreter tool instance.
|
|
32
|
+
|
|
33
|
+
⚠️ SECURITY WARNING:
|
|
34
|
+
This tool executes arbitrary Python code. By default, it runs in a
|
|
35
|
+
restricted environment with limited builtins. However, it should only
|
|
36
|
+
be used in sandboxed environments.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
allowed_modules: List of module names to allow importing.
|
|
40
|
+
Defaults to safe standard library modules.
|
|
41
|
+
allow_file_access: Whether to allow file system access. Default False.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Configured Tool instance for Python code execution.
|
|
45
|
+
"""
|
|
46
|
+
# Default safe modules
|
|
47
|
+
if allowed_modules is None:
|
|
48
|
+
allowed_modules = [
|
|
49
|
+
"math",
|
|
50
|
+
"json",
|
|
51
|
+
"re",
|
|
52
|
+
"datetime",
|
|
53
|
+
"collections",
|
|
54
|
+
"itertools",
|
|
55
|
+
"random",
|
|
56
|
+
"statistics",
|
|
57
|
+
"typing",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
def execute_python_code(args: PythonInterpreterInput) -> str:
|
|
61
|
+
"""Execute Python code in a restricted environment."""
|
|
62
|
+
max(1, min(args.timeout, 60))
|
|
63
|
+
|
|
64
|
+
# Create restricted globals
|
|
65
|
+
restricted_globals: Dict[str, Any] = {
|
|
66
|
+
"__builtins__": {
|
|
67
|
+
"print": print,
|
|
68
|
+
"len": len,
|
|
69
|
+
"range": range,
|
|
70
|
+
"enumerate": enumerate,
|
|
71
|
+
"zip": zip,
|
|
72
|
+
"map": map,
|
|
73
|
+
"filter": filter,
|
|
74
|
+
"sum": sum,
|
|
75
|
+
"min": min,
|
|
76
|
+
"max": max,
|
|
77
|
+
"abs": abs,
|
|
78
|
+
"round": round,
|
|
79
|
+
"pow": pow,
|
|
80
|
+
"divmod": divmod,
|
|
81
|
+
"bool": bool,
|
|
82
|
+
"int": int,
|
|
83
|
+
"float": float,
|
|
84
|
+
"str": str,
|
|
85
|
+
"list": list,
|
|
86
|
+
"dict": dict,
|
|
87
|
+
"tuple": tuple,
|
|
88
|
+
"set": set,
|
|
89
|
+
"frozenset": frozenset,
|
|
90
|
+
"type": type,
|
|
91
|
+
"isinstance": isinstance,
|
|
92
|
+
"hasattr": hasattr,
|
|
93
|
+
"getattr": getattr,
|
|
94
|
+
"Exception": Exception,
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# Import allowed modules
|
|
99
|
+
for module_name in allowed_modules:
|
|
100
|
+
try:
|
|
101
|
+
module = __import__(module_name)
|
|
102
|
+
restricted_globals[module_name] = module
|
|
103
|
+
except ImportError:
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
# Capture stdout
|
|
107
|
+
old_stdout = sys.stdout
|
|
108
|
+
sys.stdout = io.StringIO()
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
# Execute code with timeout-like protection (via limited operations)
|
|
112
|
+
# Note: For true sandboxing, consider using subprocess or RestrictedPython
|
|
113
|
+
exec(args.code, restricted_globals)
|
|
114
|
+
|
|
115
|
+
# Get output
|
|
116
|
+
output = sys.stdout.getvalue()
|
|
117
|
+
|
|
118
|
+
if not output.strip():
|
|
119
|
+
return "Code executed successfully (no output)."
|
|
120
|
+
|
|
121
|
+
return f"Output:\n{output}"
|
|
122
|
+
|
|
123
|
+
except Exception as e:
|
|
124
|
+
error_msg = f"Error executing code: {type(e).__name__}: {str(e)}"
|
|
125
|
+
# Include traceback for debugging
|
|
126
|
+
tb = traceback.format_exc()
|
|
127
|
+
return f"{error_msg}\n\nTraceback:\n{tb}"
|
|
128
|
+
finally:
|
|
129
|
+
sys.stdout = old_stdout
|
|
130
|
+
|
|
131
|
+
return Tool(
|
|
132
|
+
name="python_interpreter",
|
|
133
|
+
description="Execute Python code in a restricted environment. "
|
|
134
|
+
"Available: math, json, re, datetime, collections, itertools. "
|
|
135
|
+
"Use print() to see output. "
|
|
136
|
+
"⚠️ Security: Code runs in restricted mode but use with caution.",
|
|
137
|
+
parameters=PythonInterpreterInput,
|
|
138
|
+
execute=execute_python_code,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# Convenience instance with default restrictions
|
|
143
|
+
python_interpreter_tool = create_python_interpreter_tool()
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Built-in search tools using mainstream APIs.
|
|
2
|
+
|
|
3
|
+
This module provides search capabilities through:
|
|
4
|
+
- Tavily AI (high-quality AI search, requires API key)
|
|
5
|
+
- DuckDuckGo (free, no API key required)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
13
|
+
from tinyflow.core.tools import Tool
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TavilySearchInput(BaseModel):
|
|
17
|
+
"""Input parameters for Tavily search."""
|
|
18
|
+
|
|
19
|
+
query: str = Field(description="The search query to execute")
|
|
20
|
+
max_results: int = Field(default=5, description="Maximum number of results to return (1-10)")
|
|
21
|
+
include_answer: bool = Field(
|
|
22
|
+
default=True, description="Include an AI-generated answer summary"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DuckDuckGoSearchInput(BaseModel):
|
|
27
|
+
"""Input parameters for DuckDuckGo search."""
|
|
28
|
+
|
|
29
|
+
query: str = Field(description="The search query to execute")
|
|
30
|
+
max_results: int = Field(default=5, description="Maximum number of results to return")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _get_tavily_client(api_key: Optional[str] = None):
|
|
34
|
+
"""Get or create Tavily client with the provided or environment API key."""
|
|
35
|
+
try:
|
|
36
|
+
from tavily import TavilyClient
|
|
37
|
+
except ImportError:
|
|
38
|
+
raise ImportError(
|
|
39
|
+
"tavily-python is required for TavilySearchTool. "
|
|
40
|
+
"Install with: pip install tavily-python"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Configuration precedence: Manual > Environment Variable > Error
|
|
44
|
+
key = api_key or os.getenv("TAVILY_API_KEY")
|
|
45
|
+
if not key:
|
|
46
|
+
raise ValueError(
|
|
47
|
+
"Tavily API key required. Set TAVILY_API_KEY environment variable "
|
|
48
|
+
"or pass api_key parameter to the tool."
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
return TavilyClient(api_key=key)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def create_tavily_search_tool(api_key: Optional[str] = None) -> Tool:
|
|
55
|
+
"""Create a Tavily search tool instance.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
api_key: Tavily API key. If not provided, uses TAVILY_API_KEY env var.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Configured Tool instance for Tavily search.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def execute_tavily_search(args: TavilySearchInput) -> str:
|
|
65
|
+
"""Execute Tavily search and format results."""
|
|
66
|
+
client = _get_tavily_client(api_key)
|
|
67
|
+
|
|
68
|
+
response = client.search(
|
|
69
|
+
query=args.query,
|
|
70
|
+
max_results=args.max_results,
|
|
71
|
+
include_answer=args.include_answer,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Format results
|
|
75
|
+
results_text = f"Search results for: {args.query}\n\n"
|
|
76
|
+
|
|
77
|
+
if args.include_answer and "answer" in response:
|
|
78
|
+
results_text += f"AI Summary: {response['answer']}\n\n"
|
|
79
|
+
|
|
80
|
+
results = response.get("results", [])
|
|
81
|
+
for i, result in enumerate(results, 1):
|
|
82
|
+
title = result.get("title", "No title")
|
|
83
|
+
content = result.get("content", "No content")
|
|
84
|
+
url = result.get("url", "")
|
|
85
|
+
results_text += f"{i}. {title}\n{content}\nURL: {url}\n\n"
|
|
86
|
+
|
|
87
|
+
return results_text.strip()
|
|
88
|
+
|
|
89
|
+
return Tool(
|
|
90
|
+
name="tavily_search",
|
|
91
|
+
description="Search the web using Tavily AI for high-quality, relevant results. "
|
|
92
|
+
"Best for research, fact-checking, and finding current information.",
|
|
93
|
+
parameters=TavilySearchInput,
|
|
94
|
+
execute=execute_tavily_search,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def create_duckduckgo_search_tool() -> Tool:
|
|
99
|
+
"""Create a DuckDuckGo search tool instance.
|
|
100
|
+
|
|
101
|
+
Note: DuckDuckGo search does not require an API key.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Configured Tool instance for DuckDuckGo search.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def execute_duckduckgo_search(args: DuckDuckGoSearchInput) -> str:
|
|
108
|
+
"""Execute DuckDuckGo search and format results."""
|
|
109
|
+
try:
|
|
110
|
+
from duckduckgo_search import DDGS
|
|
111
|
+
except ImportError:
|
|
112
|
+
raise ImportError(
|
|
113
|
+
"duckduckgo-search is required for DuckDuckGoSearchTool. "
|
|
114
|
+
"Install with: pip install duckduckgo-search"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
with DDGS() as ddgs:
|
|
118
|
+
results = list(ddgs.text(args.query, max_results=args.max_results))
|
|
119
|
+
|
|
120
|
+
# Format results
|
|
121
|
+
results_text = f"Search results for: {args.query}\n\n"
|
|
122
|
+
|
|
123
|
+
if not results:
|
|
124
|
+
return f"No results found for: {args.query}"
|
|
125
|
+
|
|
126
|
+
for i, result in enumerate(results, 1):
|
|
127
|
+
title = result.get("title", "No title")
|
|
128
|
+
body = result.get("body", "No description")
|
|
129
|
+
href = result.get("href", "")
|
|
130
|
+
results_text += f"{i}. {title}\n{body}\nURL: {href}\n\n"
|
|
131
|
+
|
|
132
|
+
return results_text.strip()
|
|
133
|
+
|
|
134
|
+
return Tool(
|
|
135
|
+
name="duckduckgo_search",
|
|
136
|
+
description="Search the web using DuckDuckGo. Free, no API key required. "
|
|
137
|
+
"Good for general web searches when Tavily is not available.",
|
|
138
|
+
parameters=DuckDuckGoSearchInput,
|
|
139
|
+
execute=execute_duckduckgo_search,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# Convenience instances (will use env vars for API keys)
|
|
144
|
+
tavily_search_tool = create_tavily_search_tool()
|
|
145
|
+
duckduckgo_search_tool = create_duckduckgo_search_tool()
|