flowllm 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.
- flowllm-0.1.0.dist-info/METADATA +597 -0
- flowllm-0.1.0.dist-info/RECORD +66 -0
- flowllm-0.1.0.dist-info/WHEEL +5 -0
- flowllm-0.1.0.dist-info/entry_points.txt +3 -0
- flowllm-0.1.0.dist-info/licenses/LICENSE +201 -0
- flowllm-0.1.0.dist-info/top_level.txt +1 -0
- llmflow/__init__.py +0 -0
- llmflow/app.py +53 -0
- llmflow/config/__init__.py +0 -0
- llmflow/config/config_parser.py +80 -0
- llmflow/config/mock_config.yaml +58 -0
- llmflow/embedding_model/__init__.py +5 -0
- llmflow/embedding_model/base_embedding_model.py +104 -0
- llmflow/embedding_model/openai_compatible_embedding_model.py +95 -0
- llmflow/enumeration/__init__.py +0 -0
- llmflow/enumeration/agent_state.py +8 -0
- llmflow/enumeration/chunk_enum.py +9 -0
- llmflow/enumeration/http_enum.py +9 -0
- llmflow/enumeration/role.py +8 -0
- llmflow/llm/__init__.py +5 -0
- llmflow/llm/base_llm.py +138 -0
- llmflow/llm/openai_compatible_llm.py +283 -0
- llmflow/mcp_server.py +110 -0
- llmflow/op/__init__.py +10 -0
- llmflow/op/base_op.py +125 -0
- llmflow/op/mock_op.py +40 -0
- llmflow/op/prompt_mixin.py +74 -0
- llmflow/op/react/__init__.py +0 -0
- llmflow/op/react/react_v1_op.py +88 -0
- llmflow/op/react/react_v1_prompt.yaml +28 -0
- llmflow/op/vector_store/__init__.py +13 -0
- llmflow/op/vector_store/recall_vector_store_op.py +48 -0
- llmflow/op/vector_store/update_vector_store_op.py +28 -0
- llmflow/op/vector_store/vector_store_action_op.py +46 -0
- llmflow/pipeline/__init__.py +0 -0
- llmflow/pipeline/pipeline.py +94 -0
- llmflow/pipeline/pipeline_context.py +37 -0
- llmflow/schema/__init__.py +0 -0
- llmflow/schema/app_config.py +69 -0
- llmflow/schema/experience.py +144 -0
- llmflow/schema/message.py +68 -0
- llmflow/schema/request.py +32 -0
- llmflow/schema/response.py +29 -0
- llmflow/schema/vector_node.py +11 -0
- llmflow/service/__init__.py +0 -0
- llmflow/service/llmflow_service.py +96 -0
- llmflow/tool/__init__.py +9 -0
- llmflow/tool/base_tool.py +80 -0
- llmflow/tool/code_tool.py +43 -0
- llmflow/tool/dashscope_search_tool.py +162 -0
- llmflow/tool/mcp_tool.py +77 -0
- llmflow/tool/tavily_search_tool.py +109 -0
- llmflow/tool/terminate_tool.py +23 -0
- llmflow/utils/__init__.py +0 -0
- llmflow/utils/common_utils.py +17 -0
- llmflow/utils/file_handler.py +25 -0
- llmflow/utils/http_client.py +156 -0
- llmflow/utils/op_utils.py +102 -0
- llmflow/utils/registry.py +33 -0
- llmflow/utils/singleton.py +9 -0
- llmflow/utils/timer.py +53 -0
- llmflow/vector_store/__init__.py +7 -0
- llmflow/vector_store/base_vector_store.py +136 -0
- llmflow/vector_store/chroma_vector_store.py +188 -0
- llmflow/vector_store/es_vector_store.py +227 -0
- llmflow/vector_store/file_vector_store.py +163 -0
@@ -0,0 +1,144 @@
|
|
1
|
+
import datetime
|
2
|
+
from abc import ABC
|
3
|
+
from typing import List
|
4
|
+
from uuid import uuid4
|
5
|
+
|
6
|
+
from loguru import logger
|
7
|
+
from pydantic import BaseModel, Field
|
8
|
+
|
9
|
+
from llmflow.schema.vector_node import VectorNode
|
10
|
+
|
11
|
+
|
12
|
+
class ExperienceMeta(BaseModel):
|
13
|
+
author: str = Field(default="")
|
14
|
+
created_time: str = Field(default_factory=lambda: datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
15
|
+
modified_time: str = Field(default_factory=lambda: datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
16
|
+
extra_info: dict | None = Field(default=None)
|
17
|
+
|
18
|
+
def update_modified_time(self):
|
19
|
+
self.modified_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
20
|
+
|
21
|
+
|
22
|
+
class BaseExperience(BaseModel, ABC):
|
23
|
+
workspace_id: str = Field(default="")
|
24
|
+
|
25
|
+
experience_id: str = Field(default_factory=lambda: uuid4().hex)
|
26
|
+
experience_type: str = Field(default="")
|
27
|
+
|
28
|
+
when_to_use: str = Field(default="")
|
29
|
+
content: str | bytes = Field(default="")
|
30
|
+
score: float | None = Field(default=None)
|
31
|
+
metadata: ExperienceMeta = Field(default_factory=ExperienceMeta)
|
32
|
+
|
33
|
+
def to_vector_node(self) -> VectorNode:
|
34
|
+
raise NotImplementedError
|
35
|
+
|
36
|
+
@classmethod
|
37
|
+
def from_vector_node(cls, node: VectorNode):
|
38
|
+
raise NotImplementedError
|
39
|
+
|
40
|
+
|
41
|
+
class TextExperience(BaseExperience):
|
42
|
+
experience_type: str = Field(default="text")
|
43
|
+
|
44
|
+
def to_vector_node(self) -> VectorNode:
|
45
|
+
return VectorNode(unique_id=self.experience_id,
|
46
|
+
workspace_id=self.workspace_id,
|
47
|
+
content=self.when_to_use,
|
48
|
+
metadata={
|
49
|
+
"experience_type": self.experience_type,
|
50
|
+
"experience_content": self.content,
|
51
|
+
"score": self.score,
|
52
|
+
"metadata": self.metadata.model_dump(),
|
53
|
+
})
|
54
|
+
|
55
|
+
@classmethod
|
56
|
+
def from_vector_node(cls, node: VectorNode):
|
57
|
+
return cls(workspace_id=node.workspace_id,
|
58
|
+
experience_id=node.unique_id,
|
59
|
+
experience_type=node.metadata.get("experience_type"),
|
60
|
+
when_to_use=node.content,
|
61
|
+
content=node.metadata.get("experience_content"),
|
62
|
+
score=node.metadata.get("score"),
|
63
|
+
metadata=node.metadata.get("metadata"))
|
64
|
+
|
65
|
+
|
66
|
+
class FunctionArg(BaseModel):
|
67
|
+
arg_name: str = Field(default=...)
|
68
|
+
arg_type: str = Field(default=...)
|
69
|
+
required: bool = Field(default=True)
|
70
|
+
|
71
|
+
|
72
|
+
class Function(BaseModel):
|
73
|
+
func_code: str = Field(default=..., description="function code")
|
74
|
+
func_name: str = Field(default=..., description="function name")
|
75
|
+
func_args: List[FunctionArg] = Field(default_factory=list)
|
76
|
+
|
77
|
+
|
78
|
+
class FuncExperience(BaseExperience):
|
79
|
+
experience_type: str = Field(default="function")
|
80
|
+
functions: List[Function] = Field(default_factory=list)
|
81
|
+
|
82
|
+
|
83
|
+
class PersonalExperience(BaseExperience):
|
84
|
+
experience_type: str = Field(default="personal")
|
85
|
+
person: str = Field(default="")
|
86
|
+
topic: str = Field(default="")
|
87
|
+
|
88
|
+
|
89
|
+
class KnowledgeExperience(BaseExperience):
|
90
|
+
experience_type: str = Field(default="knowledge")
|
91
|
+
topic: str = Field(default="")
|
92
|
+
|
93
|
+
|
94
|
+
def vector_node_to_experience(node: VectorNode) -> BaseExperience:
|
95
|
+
experience_type = node.metadata.get("experience_type")
|
96
|
+
if experience_type == "text":
|
97
|
+
return TextExperience.from_vector_node(node)
|
98
|
+
|
99
|
+
elif experience_type == "function":
|
100
|
+
return FuncExperience.from_vector_node(node)
|
101
|
+
|
102
|
+
elif experience_type == "personal":
|
103
|
+
return PersonalExperience.from_vector_node(node)
|
104
|
+
|
105
|
+
elif experience_type == "knowledge":
|
106
|
+
return KnowledgeExperience.from_vector_node(node)
|
107
|
+
|
108
|
+
else:
|
109
|
+
logger.warning(f"experience type {experience_type} not supported")
|
110
|
+
return TextExperience.from_vector_node(node)
|
111
|
+
|
112
|
+
|
113
|
+
def dict_to_experience(experience_dict: dict) -> BaseExperience:
|
114
|
+
experience_type = experience_dict.get("experience_type", "text")
|
115
|
+
if experience_type == "text":
|
116
|
+
return TextExperience(**experience_dict)
|
117
|
+
|
118
|
+
elif experience_type == "function":
|
119
|
+
return FuncExperience(**experience_dict)
|
120
|
+
|
121
|
+
elif experience_type == "personal":
|
122
|
+
return PersonalExperience(**experience_dict)
|
123
|
+
|
124
|
+
elif experience_type == "knowledge":
|
125
|
+
return KnowledgeExperience(**experience_dict)
|
126
|
+
|
127
|
+
else:
|
128
|
+
logger.warning(f"experience type {experience_type} not supported")
|
129
|
+
return TextExperience(**experience_dict)
|
130
|
+
|
131
|
+
|
132
|
+
if __name__ == "__main__":
|
133
|
+
e1 = TextExperience(
|
134
|
+
workspace_id="w_1024",
|
135
|
+
experience_id="123",
|
136
|
+
when_to_use="test case use",
|
137
|
+
content="test content",
|
138
|
+
score=0.99,
|
139
|
+
metadata=ExperienceMeta(author="user"))
|
140
|
+
print(e1.model_dump_json(indent=2))
|
141
|
+
v1 = e1.to_vector_node()
|
142
|
+
print(v1.model_dump_json(indent=2))
|
143
|
+
e2 = vector_node_to_experience(v1)
|
144
|
+
print(e2.model_dump_json(indent=2))
|
@@ -0,0 +1,68 @@
|
|
1
|
+
import json
|
2
|
+
from typing import List
|
3
|
+
|
4
|
+
from pydantic import BaseModel, Field, model_validator
|
5
|
+
|
6
|
+
from llmflow.enumeration.role import Role
|
7
|
+
|
8
|
+
|
9
|
+
class ToolCall(BaseModel):
|
10
|
+
index: int = Field(default=0)
|
11
|
+
id: str = Field(default="")
|
12
|
+
name: str = Field(default="")
|
13
|
+
arguments: str = Field(default="")
|
14
|
+
type: str = Field(default="function")
|
15
|
+
|
16
|
+
@model_validator(mode="before") # noqa
|
17
|
+
@classmethod
|
18
|
+
def init_tool_call(cls, data: dict):
|
19
|
+
tool_type = data.get("type", "")
|
20
|
+
tool_type_dict = data.get(tool_type, {})
|
21
|
+
|
22
|
+
for key in ["name", "arguments"]:
|
23
|
+
if key not in data:
|
24
|
+
data[key] = tool_type_dict.get(key, "")
|
25
|
+
return data
|
26
|
+
|
27
|
+
@property
|
28
|
+
def argument_dict(self) -> dict:
|
29
|
+
return json.loads(self.arguments)
|
30
|
+
|
31
|
+
def simple_dump(self) -> dict:
|
32
|
+
return {
|
33
|
+
"id": self.id,
|
34
|
+
self.type: {
|
35
|
+
"arguments": self.arguments,
|
36
|
+
"name": self.name
|
37
|
+
},
|
38
|
+
"type": self.type,
|
39
|
+
"index": self.index,
|
40
|
+
}
|
41
|
+
|
42
|
+
class Message(BaseModel):
|
43
|
+
role: Role = Field(default=Role.USER)
|
44
|
+
content: str | bytes = Field(default="")
|
45
|
+
reasoning_content: str = Field(default="")
|
46
|
+
tool_calls: List[ToolCall] = Field(default_factory=list)
|
47
|
+
tool_call_id: str = Field(default="")
|
48
|
+
metadata: dict = Field(default_factory=dict)
|
49
|
+
|
50
|
+
def simple_dump(self, add_reason_when_empty: bool = True) -> dict:
|
51
|
+
result: dict
|
52
|
+
if self.content:
|
53
|
+
result = {"role": self.role.value, "content": self.content}
|
54
|
+
elif add_reason_when_empty and self.reasoning_content:
|
55
|
+
result = {"role": self.role.value, "content": self.reasoning_content}
|
56
|
+
else:
|
57
|
+
result = {"role": self.role.value, "content": ""}
|
58
|
+
|
59
|
+
if self.tool_calls:
|
60
|
+
result["tool_calls"] = [x.simple_dump() for x in self.tool_calls]
|
61
|
+
return result
|
62
|
+
|
63
|
+
|
64
|
+
class Trajectory(BaseModel):
|
65
|
+
task_id: str = Field(default="")
|
66
|
+
messages: List[Message] = Field(default_factory=list)
|
67
|
+
score: float = Field(default=0.0)
|
68
|
+
metadata: dict = Field(default_factory=dict)
|
@@ -0,0 +1,32 @@
|
|
1
|
+
from typing import List
|
2
|
+
|
3
|
+
from pydantic import BaseModel, Field
|
4
|
+
|
5
|
+
from llmflow.schema.message import Message, Trajectory
|
6
|
+
|
7
|
+
|
8
|
+
class BaseRequest(BaseModel):
|
9
|
+
workspace_id: str = Field(default="default")
|
10
|
+
config: dict = Field(default_factory=dict)
|
11
|
+
|
12
|
+
|
13
|
+
class RetrieverRequest(BaseRequest):
|
14
|
+
query: str = Field(default="")
|
15
|
+
messages: List[Message] = Field(default_factory=list)
|
16
|
+
top_k: int = Field(default=1)
|
17
|
+
|
18
|
+
|
19
|
+
class SummarizerRequest(BaseRequest):
|
20
|
+
traj_list: List[Trajectory] = Field(default_factory=list)
|
21
|
+
|
22
|
+
|
23
|
+
class VectorStoreRequest(BaseRequest):
|
24
|
+
action: str = Field(default="")
|
25
|
+
src_workspace_id: str = Field(default="")
|
26
|
+
path: str = Field(default="")
|
27
|
+
|
28
|
+
|
29
|
+
class AgentRequest(BaseRequest):
|
30
|
+
query: str = Field(default="")
|
31
|
+
messages: List[Message] = Field(default_factory=list)
|
32
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
from typing import List
|
2
|
+
|
3
|
+
from pydantic import BaseModel, Field
|
4
|
+
|
5
|
+
from llmflow.schema.experience import BaseExperience
|
6
|
+
from llmflow.schema.message import Message
|
7
|
+
|
8
|
+
|
9
|
+
class BaseResponse(BaseModel):
|
10
|
+
success: bool = Field(default=True)
|
11
|
+
metadata: dict = Field(default_factory=dict)
|
12
|
+
|
13
|
+
|
14
|
+
class RetrieverResponse(BaseResponse):
|
15
|
+
experience_list: List[BaseExperience] = Field(default_factory=list)
|
16
|
+
experience_merged: str = Field(default="")
|
17
|
+
|
18
|
+
|
19
|
+
class SummarizerResponse(BaseResponse):
|
20
|
+
experience_list: List[BaseExperience] = Field(default_factory=list)
|
21
|
+
deleted_experience_ids: List[str] = Field(default_factory=list)
|
22
|
+
|
23
|
+
|
24
|
+
class VectorStoreResponse(BaseResponse):
|
25
|
+
...
|
26
|
+
|
27
|
+
class AgentResponse(BaseResponse):
|
28
|
+
answer: str = Field(default="")
|
29
|
+
messages: List[Message] = Field(default_factory=list)
|
@@ -0,0 +1,11 @@
|
|
1
|
+
from typing import List
|
2
|
+
from uuid import uuid4
|
3
|
+
from pydantic import BaseModel, Field
|
4
|
+
|
5
|
+
|
6
|
+
class VectorNode(BaseModel):
|
7
|
+
unique_id: str = Field(default_factory=lambda: uuid4().hex)
|
8
|
+
workspace_id: str = Field(default="")
|
9
|
+
content: str = Field(default="")
|
10
|
+
vector: List[float] | None = Field(default=None)
|
11
|
+
metadata: dict = Field(default_factory=dict)
|
File without changes
|
@@ -0,0 +1,96 @@
|
|
1
|
+
from concurrent.futures import ThreadPoolExecutor
|
2
|
+
from typing import List
|
3
|
+
|
4
|
+
from loguru import logger
|
5
|
+
|
6
|
+
from llmflow.config.config_parser import ConfigParser
|
7
|
+
from llmflow.embedding_model import EMBEDDING_MODEL_REGISTRY
|
8
|
+
from llmflow.pipeline.pipeline import Pipeline
|
9
|
+
from llmflow.pipeline.pipeline_context import PipelineContext
|
10
|
+
from llmflow.schema.app_config import AppConfig, HttpServiceConfig, EmbeddingModelConfig
|
11
|
+
from llmflow.schema.request import SummarizerRequest, RetrieverRequest, VectorStoreRequest, AgentRequest, \
|
12
|
+
BaseRequest
|
13
|
+
from llmflow.schema.response import SummarizerResponse, RetrieverResponse, VectorStoreResponse, AgentResponse, \
|
14
|
+
BaseResponse
|
15
|
+
from llmflow.vector_store import VECTOR_STORE_REGISTRY
|
16
|
+
|
17
|
+
|
18
|
+
class LLMFlowService:
|
19
|
+
|
20
|
+
def __init__(self, args: List[str]):
|
21
|
+
self.config_parser = ConfigParser(args)
|
22
|
+
self.init_app_config: AppConfig = self.config_parser.get_app_config()
|
23
|
+
self.thread_pool = ThreadPoolExecutor(max_workers=self.init_app_config.thread_pool.max_workers)
|
24
|
+
|
25
|
+
# The vectorstore is initialized at the very beginning and then used directly afterward.
|
26
|
+
self.vector_store_dict: dict = {}
|
27
|
+
for name, config in self.init_app_config.vector_store.items():
|
28
|
+
assert config.backend in VECTOR_STORE_REGISTRY, f"backend={config.backend} is not existed"
|
29
|
+
vector_store_cls = VECTOR_STORE_REGISTRY[config.backend]
|
30
|
+
|
31
|
+
assert config.embedding_model in self.init_app_config.embedding_model, \
|
32
|
+
f"embedding_model={config.embedding_model} is not existed"
|
33
|
+
embedding_model_config: EmbeddingModelConfig = self.init_app_config.embedding_model[config.embedding_model]
|
34
|
+
|
35
|
+
assert embedding_model_config.backend in EMBEDDING_MODEL_REGISTRY, \
|
36
|
+
f"embedding_model={embedding_model_config.backend} is not existed"
|
37
|
+
embedding_model_cls = EMBEDDING_MODEL_REGISTRY[embedding_model_config.backend]
|
38
|
+
embedding_model = embedding_model_cls(model_name=embedding_model_config.model_name,
|
39
|
+
**embedding_model_config.params)
|
40
|
+
|
41
|
+
self.vector_store_dict[name] = vector_store_cls(embedding_model=embedding_model, **config.params)
|
42
|
+
|
43
|
+
@property
|
44
|
+
def http_service_config(self) -> HttpServiceConfig:
|
45
|
+
return self.init_app_config.http_service
|
46
|
+
|
47
|
+
def __call__(self, api: str, request: dict | BaseRequest) -> BaseResponse:
|
48
|
+
if isinstance(request, dict):
|
49
|
+
app_config: AppConfig = self.config_parser.get_app_config(**request["config"])
|
50
|
+
else:
|
51
|
+
app_config: AppConfig = self.config_parser.get_app_config(**request.config)
|
52
|
+
|
53
|
+
if api == "retriever":
|
54
|
+
if isinstance(request, dict):
|
55
|
+
request = RetrieverRequest(**request)
|
56
|
+
response = RetrieverResponse()
|
57
|
+
pipeline = app_config.api.retriever
|
58
|
+
|
59
|
+
elif api == "summarizer":
|
60
|
+
if isinstance(request, dict):
|
61
|
+
request = SummarizerRequest(**request)
|
62
|
+
response = SummarizerResponse()
|
63
|
+
pipeline = app_config.api.summarizer
|
64
|
+
|
65
|
+
elif api == "vector_store":
|
66
|
+
if isinstance(request, dict):
|
67
|
+
request = VectorStoreRequest(**request)
|
68
|
+
response = VectorStoreResponse()
|
69
|
+
pipeline = app_config.api.vector_store
|
70
|
+
|
71
|
+
elif api == "agent":
|
72
|
+
if isinstance(request, dict):
|
73
|
+
request = AgentRequest(**request)
|
74
|
+
response = AgentResponse()
|
75
|
+
pipeline = app_config.api.agent
|
76
|
+
|
77
|
+
else:
|
78
|
+
raise RuntimeError(f"Invalid service.api={api}")
|
79
|
+
|
80
|
+
logger.info(f"request={request.model_dump_json()}")
|
81
|
+
|
82
|
+
try:
|
83
|
+
context = PipelineContext(app_config=app_config,
|
84
|
+
thread_pool=self.thread_pool,
|
85
|
+
request=request,
|
86
|
+
response=response,
|
87
|
+
vector_store_dict=self.vector_store_dict)
|
88
|
+
pipeline = Pipeline(pipeline=pipeline, context=context)
|
89
|
+
pipeline()
|
90
|
+
|
91
|
+
except Exception as e:
|
92
|
+
logger.exception(f"api={api} encounter error={e.args}")
|
93
|
+
response.success = False
|
94
|
+
response.metadata["error"] = str(e)
|
95
|
+
|
96
|
+
return response
|
llmflow/tool/__init__.py
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
from llmflow.utils.registry import Registry
|
2
|
+
|
3
|
+
TOOL_REGISTRY = Registry()
|
4
|
+
|
5
|
+
from llmflow.tool.code_tool import CodeTool
|
6
|
+
from llmflow.tool.dashscope_search_tool import DashscopeSearchTool
|
7
|
+
from llmflow.tool.tavily_search_tool import TavilySearchTool
|
8
|
+
from llmflow.tool.terminate_tool import TerminateTool
|
9
|
+
from llmflow.tool.mcp_tool import MCPTool
|
@@ -0,0 +1,80 @@
|
|
1
|
+
from abc import ABC
|
2
|
+
|
3
|
+
from loguru import logger
|
4
|
+
from pydantic import BaseModel, Field
|
5
|
+
|
6
|
+
|
7
|
+
class BaseTool(BaseModel, ABC):
|
8
|
+
tool_id: str = Field(default="")
|
9
|
+
name: str = Field(..., description="tool name")
|
10
|
+
description: str = Field(..., description="tool description")
|
11
|
+
tool_type: str = Field(default="function")
|
12
|
+
parameters: dict = Field(default_factory=dict, description="tool parameters")
|
13
|
+
arguments: dict = Field(default_factory=dict, description="execute arguments")
|
14
|
+
|
15
|
+
enable_cache: bool = Field(default=False, description="whether to cache the tool result")
|
16
|
+
cached_result: dict = Field(default_factory=dict, description="tool execution result")
|
17
|
+
|
18
|
+
max_retries: int = Field(default=3, description="max retries")
|
19
|
+
raise_exception: bool = Field(default=True, description="raise exception")
|
20
|
+
success: bool = Field(default=True, description="whether the tool executed successfully")
|
21
|
+
|
22
|
+
def reset(self):
|
23
|
+
self.arguments.clear()
|
24
|
+
self.cached_result.clear()
|
25
|
+
self.success = True
|
26
|
+
|
27
|
+
def _execute(self, **kwargs):
|
28
|
+
raise NotImplementedError
|
29
|
+
|
30
|
+
def execute(self, **kwargs):
|
31
|
+
cache_id = ""
|
32
|
+
if self.enable_cache:
|
33
|
+
cache_id = self.get_cache_id(**kwargs)
|
34
|
+
if cache_id in self.cached_result:
|
35
|
+
return self.cached_result[cache_id]
|
36
|
+
|
37
|
+
for i in range(self.max_retries):
|
38
|
+
try:
|
39
|
+
if self.enable_cache:
|
40
|
+
self.cached_result[cache_id] = self._execute(**kwargs)
|
41
|
+
return self.cached_result[cache_id]
|
42
|
+
|
43
|
+
else:
|
44
|
+
return self._execute(**kwargs)
|
45
|
+
|
46
|
+
except Exception as e:
|
47
|
+
logger.exception(f"using tool.name={self.name} encounter error with e={e.args}")
|
48
|
+
if i == self.max_retries - 1 and self.raise_exception:
|
49
|
+
raise e
|
50
|
+
|
51
|
+
return None
|
52
|
+
|
53
|
+
|
54
|
+
def simple_dump(self) -> dict:
|
55
|
+
"""
|
56
|
+
It may be in other different tool params formats; different versions are completed here.
|
57
|
+
"""
|
58
|
+
return {
|
59
|
+
"type": self.tool_type,
|
60
|
+
self.tool_type: {
|
61
|
+
"name": self.name,
|
62
|
+
"description": self.description,
|
63
|
+
"parameters": self.parameters,
|
64
|
+
},
|
65
|
+
}
|
66
|
+
|
67
|
+
@property
|
68
|
+
def input_schema(self) -> dict:
|
69
|
+
return self.parameters.get("properties", {})
|
70
|
+
|
71
|
+
@property
|
72
|
+
def output_schema(self) -> dict:
|
73
|
+
raise NotImplementedError
|
74
|
+
|
75
|
+
def refresh(self):
|
76
|
+
# for mcp
|
77
|
+
raise NotImplementedError
|
78
|
+
|
79
|
+
def get_cache_id(self, **kwargs) -> str:
|
80
|
+
raise NotImplementedError
|
@@ -0,0 +1,43 @@
|
|
1
|
+
import sys
|
2
|
+
from io import StringIO
|
3
|
+
|
4
|
+
from llmflow.tool import TOOL_REGISTRY
|
5
|
+
from llmflow.tool.base_tool import BaseTool
|
6
|
+
|
7
|
+
|
8
|
+
@TOOL_REGISTRY.register()
|
9
|
+
class CodeTool(BaseTool):
|
10
|
+
name: str = "python_execute"
|
11
|
+
description: str = "Execute python code can be used in scenarios such as analysis or calculation, and the final result can be printed using the `print` function."
|
12
|
+
parameters: dict = {
|
13
|
+
"type": "object",
|
14
|
+
"properties": {
|
15
|
+
"code": {
|
16
|
+
"type": "string",
|
17
|
+
"description": "code to be executed. Please do not execute any matplotlib code here.",
|
18
|
+
}
|
19
|
+
},
|
20
|
+
"required": ["code"]
|
21
|
+
}
|
22
|
+
|
23
|
+
def _execute(self, code: str, **kwargs):
|
24
|
+
old_stdout = sys.stdout
|
25
|
+
redirected_output = sys.stdout = StringIO()
|
26
|
+
|
27
|
+
try:
|
28
|
+
exec(code)
|
29
|
+
result = redirected_output.getvalue()
|
30
|
+
|
31
|
+
except Exception as e:
|
32
|
+
self.success = False
|
33
|
+
result = str(e)
|
34
|
+
|
35
|
+
sys.stdout = old_stdout
|
36
|
+
|
37
|
+
return result
|
38
|
+
|
39
|
+
|
40
|
+
if __name__ == '__main__':
|
41
|
+
tool = CodeTool()
|
42
|
+
print(tool.execute(code="print('Hello World')"))
|
43
|
+
print(tool.execute(code="print('Hello World!'"))
|
@@ -0,0 +1,162 @@
|
|
1
|
+
import os
|
2
|
+
from typing import Literal
|
3
|
+
|
4
|
+
import dashscope
|
5
|
+
from dashscope.api_entities.dashscope_response import Message
|
6
|
+
from dotenv import load_dotenv
|
7
|
+
from loguru import logger
|
8
|
+
from pydantic import Field
|
9
|
+
|
10
|
+
from llmflow.tool import TOOL_REGISTRY
|
11
|
+
from llmflow.tool.base_tool import BaseTool
|
12
|
+
|
13
|
+
|
14
|
+
@TOOL_REGISTRY.register()
|
15
|
+
class DashscopeSearchTool(BaseTool):
|
16
|
+
name: str = "web_search"
|
17
|
+
description: str = "Use search keywords to retrieve relevant information from the internet. " \
|
18
|
+
"If there are multiple search keywords, please use each keyword separately to call this tool."
|
19
|
+
parameters: dict = {
|
20
|
+
"type": "object",
|
21
|
+
"properties": {
|
22
|
+
"query": {
|
23
|
+
"type": "string",
|
24
|
+
"description": "search keyword",
|
25
|
+
}
|
26
|
+
},
|
27
|
+
"required": ["query"]
|
28
|
+
}
|
29
|
+
|
30
|
+
model_name: Literal["qwen-plus-2025-04-28", "qwq-plus-latest", "qwen-max-2025-01-25"] = \
|
31
|
+
Field(default="qwen-plus-2025-04-28")
|
32
|
+
api_key: str = Field(default_factory=lambda: os.environ["DASHSCOPE_API_KEY"])
|
33
|
+
stream_print: bool = Field(default=False)
|
34
|
+
temperature: float = Field(default=0.0000001)
|
35
|
+
use_role_prompt: bool = Field(default=True)
|
36
|
+
role_prompt: str = """
|
37
|
+
# user's question
|
38
|
+
{question}
|
39
|
+
|
40
|
+
# task
|
41
|
+
Extract the original content related to the user's question directly from the context, maintain accuracy, and avoid excessive processing. """.strip()
|
42
|
+
return_only_content: bool = Field(default=True)
|
43
|
+
|
44
|
+
def parse_reasoning_response(self, response, result: dict):
|
45
|
+
is_answering = False
|
46
|
+
is_first_chunk = True
|
47
|
+
|
48
|
+
for chunk in response:
|
49
|
+
if is_first_chunk:
|
50
|
+
result["search_results"] = chunk.output.search_info["search_results"]
|
51
|
+
|
52
|
+
if self.stream_print:
|
53
|
+
print("=" * 20 + "search result" + "=" * 20)
|
54
|
+
for web in result["search_results"]:
|
55
|
+
print(f"[{web['index']}]: [{web['title']}]({web['url']})")
|
56
|
+
print("=" * 20 + "thinking process" + "=" * 20)
|
57
|
+
result["reasoning_content"] += chunk.output.choices[0].message.reasoning_content
|
58
|
+
|
59
|
+
if self.stream_print:
|
60
|
+
print(chunk.output.choices[0].message.reasoning_content, end="", flush=True)
|
61
|
+
is_first_chunk = False
|
62
|
+
|
63
|
+
else:
|
64
|
+
if chunk.output.choices[0].message.content == "" \
|
65
|
+
and chunk.output.choices[0].message.reasoning_content == "":
|
66
|
+
pass
|
67
|
+
|
68
|
+
else:
|
69
|
+
if chunk.output.choices[0].message.reasoning_content != "" and \
|
70
|
+
chunk.output.choices[0].message.content == "":
|
71
|
+
|
72
|
+
if self.stream_print:
|
73
|
+
print(chunk.output.choices[0].message.reasoning_content, end="", flush=True)
|
74
|
+
result["reasoning_content"] += chunk.output.choices[0].message.reasoning_content
|
75
|
+
|
76
|
+
elif chunk.output.choices[0].message.content != "":
|
77
|
+
if not is_answering:
|
78
|
+
if self.stream_print:
|
79
|
+
print("\n" + "=" * 20 + "complete answer" + "=" * 20)
|
80
|
+
is_answering = True
|
81
|
+
|
82
|
+
if self.stream_print:
|
83
|
+
print(chunk.output.choices[0].message.content, end="", flush=True)
|
84
|
+
result["answer_content"] += chunk.output.choices[0].message.content
|
85
|
+
|
86
|
+
def parse_response(self, response, result: dict):
|
87
|
+
is_first_chunk = True
|
88
|
+
|
89
|
+
for chunk in response:
|
90
|
+
if is_first_chunk:
|
91
|
+
result["search_results"] = chunk.output.search_info["search_results"]
|
92
|
+
|
93
|
+
if self.stream_print:
|
94
|
+
print("=" * 20 + "search result" + "=" * 20)
|
95
|
+
for web in result["search_results"]:
|
96
|
+
print(f"[{web['index']}]: [{web['title']}]({web['url']})")
|
97
|
+
print("\n" + "=" * 20 + "complete answer" + "=" * 20)
|
98
|
+
is_first_chunk = False
|
99
|
+
|
100
|
+
else:
|
101
|
+
if chunk.output.choices[0].message.content == "":
|
102
|
+
pass
|
103
|
+
|
104
|
+
else:
|
105
|
+
if chunk.output.choices[0].message.content != "":
|
106
|
+
if self.stream_print:
|
107
|
+
print(chunk.output.choices[0].message.content, end="", flush=True)
|
108
|
+
result["answer_content"] += chunk.output.choices[0].message.content
|
109
|
+
|
110
|
+
def execute(self, query: str = "", **kwargs):
|
111
|
+
result = {
|
112
|
+
"search_results": [],
|
113
|
+
"reasoning_content": "",
|
114
|
+
"answer_content": ""
|
115
|
+
}
|
116
|
+
user_query = self.role_prompt.format(question=query) if self.use_role_prompt else query
|
117
|
+
messages = [Message(role="user", content=user_query)]
|
118
|
+
|
119
|
+
response = dashscope.Generation.call(
|
120
|
+
api_key=self.api_key,
|
121
|
+
model=self.model_name,
|
122
|
+
messages=messages,
|
123
|
+
enable_thinking=True,
|
124
|
+
enable_search=True,
|
125
|
+
search_options={
|
126
|
+
"forced_search": True,
|
127
|
+
"enable_source": True,
|
128
|
+
"enable_citation": False,
|
129
|
+
"search_strategy": "pro"
|
130
|
+
},
|
131
|
+
stream=True,
|
132
|
+
incremental_output=True,
|
133
|
+
result_format="message",
|
134
|
+
)
|
135
|
+
|
136
|
+
if self.model_name != "qwen-max-2025-01-25":
|
137
|
+
self.parse_reasoning_response(response, result)
|
138
|
+
else:
|
139
|
+
self.parse_response(response, result)
|
140
|
+
|
141
|
+
if self.return_only_content:
|
142
|
+
return result["answer_content"]
|
143
|
+
else:
|
144
|
+
return result
|
145
|
+
|
146
|
+
|
147
|
+
def main():
|
148
|
+
load_dotenv()
|
149
|
+
query = "What is artificial intelligence?"
|
150
|
+
|
151
|
+
tool = DashscopeSearchTool(stream_print=True)
|
152
|
+
logger.info(tool.execute(query=query))
|
153
|
+
|
154
|
+
tool = DashscopeSearchTool(stream_print=False)
|
155
|
+
logger.info(tool.execute(query=query))
|
156
|
+
|
157
|
+
tool = DashscopeSearchTool(stream_print=True, model_name="qwen-max-2025-01-25")
|
158
|
+
logger.info(tool.execute(query=query))
|
159
|
+
|
160
|
+
|
161
|
+
if __name__ == '__main__':
|
162
|
+
main()
|