flowllm 0.1.1__py3-none-any.whl → 0.1.3__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/__init__.py +19 -6
- flowllm/app.py +4 -14
- flowllm/client/__init__.py +25 -0
- flowllm/client/async_http_client.py +81 -0
- flowllm/client/http_client.py +81 -0
- flowllm/client/mcp_client.py +133 -0
- flowllm/client/sync_mcp_client.py +116 -0
- flowllm/config/__init__.py +1 -0
- flowllm/config/{default_config.yaml → default.yaml} +3 -8
- flowllm/config/empty.yaml +37 -0
- flowllm/config/pydantic_config_parser.py +17 -17
- flowllm/context/base_context.py +27 -7
- flowllm/context/flow_context.py +6 -18
- flowllm/context/registry.py +5 -1
- flowllm/context/service_context.py +83 -37
- flowllm/embedding_model/__init__.py +1 -1
- flowllm/embedding_model/base_embedding_model.py +91 -0
- flowllm/embedding_model/openai_compatible_embedding_model.py +63 -5
- flowllm/flow/__init__.py +1 -0
- flowllm/flow/base_flow.py +74 -0
- flowllm/flow/base_tool_flow.py +15 -0
- flowllm/flow/gallery/__init__.py +8 -0
- flowllm/flow/gallery/cmd_flow.py +11 -0
- flowllm/flow/gallery/code_tool_flow.py +30 -0
- flowllm/flow/gallery/dashscope_search_tool_flow.py +34 -0
- flowllm/flow/gallery/deepsearch_tool_flow.py +39 -0
- flowllm/flow/gallery/expression_tool_flow.py +18 -0
- flowllm/flow/gallery/mock_tool_flow.py +62 -0
- flowllm/flow/gallery/tavily_search_tool_flow.py +30 -0
- flowllm/flow/gallery/terminate_tool_flow.py +30 -0
- flowllm/flow/parser/__init__.py +0 -0
- flowllm/{flow_engine/simple_flow_engine.py → flow/parser/expression_parser.py} +25 -67
- flowllm/llm/__init__.py +2 -1
- flowllm/llm/base_llm.py +94 -4
- flowllm/llm/litellm_llm.py +456 -0
- flowllm/llm/openai_compatible_llm.py +205 -5
- flowllm/op/__init__.py +12 -3
- flowllm/op/agent/__init__.py +1 -0
- flowllm/op/agent/react_v1_op.py +109 -0
- flowllm/op/agent/react_v1_prompt.yaml +54 -0
- flowllm/op/agent/react_v2_op.py +86 -0
- flowllm/op/agent/react_v2_prompt.yaml +35 -0
- flowllm/op/akshare/__init__.py +3 -0
- flowllm/op/akshare/get_ak_a_code_op.py +14 -22
- flowllm/op/akshare/get_ak_a_info_op.py +17 -20
- flowllm/op/{llm_base_op.py → base_llm_op.py} +7 -5
- flowllm/op/base_op.py +40 -44
- flowllm/op/base_ray_op.py +313 -0
- flowllm/op/code/__init__.py +1 -0
- flowllm/op/code/execute_code_op.py +42 -0
- flowllm/op/gallery/__init__.py +2 -0
- flowllm/op/{mock_op.py → gallery/mock_op.py} +4 -4
- flowllm/op/gallery/terminate_op.py +29 -0
- flowllm/op/parallel_op.py +2 -9
- flowllm/op/search/__init__.py +3 -0
- flowllm/op/search/dashscope_deep_research_op.py +267 -0
- flowllm/op/search/dashscope_search_op.py +186 -0
- flowllm/op/search/dashscope_search_prompt.yaml +13 -0
- flowllm/op/search/tavily_search_op.py +109 -0
- flowllm/op/sequential_op.py +1 -9
- flowllm/schema/flow_request.py +12 -0
- flowllm/schema/message.py +2 -0
- flowllm/schema/service_config.py +12 -16
- flowllm/schema/tool_call.py +20 -8
- flowllm/schema/vector_node.py +1 -0
- flowllm/service/__init__.py +3 -2
- flowllm/service/base_service.py +50 -41
- flowllm/service/cmd_service.py +15 -0
- flowllm/service/http_service.py +34 -42
- flowllm/service/mcp_service.py +13 -11
- flowllm/storage/cache/__init__.py +1 -0
- flowllm/storage/cache/cache_data_handler.py +104 -0
- flowllm/{utils/dataframe_cache.py → storage/cache/data_cache.py} +136 -92
- flowllm/storage/vector_store/__init__.py +3 -3
- flowllm/storage/vector_store/base_vector_store.py +3 -0
- flowllm/storage/vector_store/es_vector_store.py +4 -5
- flowllm/storage/vector_store/local_vector_store.py +0 -1
- flowllm/utils/common_utils.py +9 -21
- flowllm/utils/fetch_url.py +16 -12
- flowllm/utils/llm_utils.py +28 -0
- flowllm/utils/logger_utils.py +28 -0
- flowllm/utils/ridge_v2.py +54 -0
- {flowllm-0.1.1.dist-info → flowllm-0.1.3.dist-info}/METADATA +43 -390
- flowllm-0.1.3.dist-info/RECORD +102 -0
- flowllm-0.1.3.dist-info/entry_points.txt +2 -0
- flowllm/flow_engine/__init__.py +0 -1
- flowllm/flow_engine/base_flow_engine.py +0 -34
- flowllm-0.1.1.dist-info/RECORD +0 -62
- flowllm-0.1.1.dist-info/entry_points.txt +0 -4
- {flowllm-0.1.1.dist-info → flowllm-0.1.3.dist-info}/WHEEL +0 -0
- {flowllm-0.1.1.dist-info → flowllm-0.1.3.dist-info}/licenses/LICENSE +0 -0
- {flowllm-0.1.1.dist-info → flowllm-0.1.3.dist-info}/top_level.txt +0 -0
flowllm/context/base_context.py
CHANGED
@@ -1,13 +1,20 @@
|
|
1
|
-
from typing import List
|
2
|
-
|
3
|
-
|
4
1
|
class BaseContext:
|
5
2
|
def __init__(self, **kwargs):
|
6
3
|
self._data = {**kwargs}
|
7
4
|
|
8
5
|
def __getattr__(self, name: str):
|
9
|
-
|
10
|
-
|
6
|
+
# Avoid infinite recursion when _data is not yet initialized
|
7
|
+
if name == '_data':
|
8
|
+
raise AttributeError(f"'{self.__class__.__name__}' has no attribute '{name}'")
|
9
|
+
|
10
|
+
# Use object.__getattribute__ to safely access _data
|
11
|
+
try:
|
12
|
+
data = object.__getattribute__(self, '_data')
|
13
|
+
except AttributeError:
|
14
|
+
raise AttributeError(f"'{self.__class__.__name__}' has no attribute '{name}'")
|
15
|
+
|
16
|
+
if name in data:
|
17
|
+
return data[name]
|
11
18
|
|
12
19
|
raise AttributeError(f"'{self.__class__.__name__}' has no attribute '{name}'")
|
13
20
|
|
@@ -34,13 +41,26 @@ class BaseContext:
|
|
34
41
|
def dump(self) -> dict:
|
35
42
|
return self._data
|
36
43
|
|
44
|
+
def get(self, key: str, default=None):
|
45
|
+
return self._data.get(key, default)
|
46
|
+
|
37
47
|
@property
|
38
|
-
def keys(self)
|
39
|
-
return
|
48
|
+
def keys(self):
|
49
|
+
return self._data.keys()
|
40
50
|
|
41
51
|
def update(self, **kwargs):
|
42
52
|
self._data.update(kwargs)
|
43
53
|
|
54
|
+
def items(self):
|
55
|
+
return self._data.items()
|
56
|
+
|
57
|
+
def __getstate__(self):
|
58
|
+
"""Support for pickle serialization"""
|
59
|
+
return {'_data': self._data}
|
60
|
+
|
61
|
+
def __setstate__(self, state):
|
62
|
+
"""Support for pickle deserialization"""
|
63
|
+
self._data = state['_data']
|
44
64
|
|
45
65
|
if __name__ == "__main__":
|
46
66
|
ctx = BaseContext(**{"name": "Alice", "age": 30, "city": "New York"})
|
flowllm/context/flow_context.py
CHANGED
@@ -2,27 +2,15 @@ import uuid
|
|
2
2
|
|
3
3
|
from flowllm.context.base_context import BaseContext
|
4
4
|
from flowllm.schema.flow_response import FlowResponse
|
5
|
-
from flowllm.schema.service_config import ServiceConfig
|
6
5
|
|
7
6
|
|
8
7
|
class FlowContext(BaseContext):
|
9
8
|
|
10
|
-
def __init__(self,
|
9
|
+
def __init__(self,
|
10
|
+
flow_id: str = uuid.uuid4().hex,
|
11
|
+
response: FlowResponse = None,
|
12
|
+
**kwargs):
|
11
13
|
super().__init__(**kwargs)
|
12
|
-
self.flow_id: str = flow_id
|
13
|
-
|
14
|
-
@property
|
15
|
-
def service_config(self) -> ServiceConfig:
|
16
|
-
return self._data.get("service_config")
|
17
|
-
|
18
|
-
@service_config.setter
|
19
|
-
def service_config(self, service_config: ServiceConfig):
|
20
|
-
self._data["service_config"] = service_config
|
21
14
|
|
22
|
-
|
23
|
-
|
24
|
-
return self._data.get("response")
|
25
|
-
|
26
|
-
@response.setter
|
27
|
-
def response(self, response: FlowResponse):
|
28
|
-
self._data["response"] = response
|
15
|
+
self.flow_id: str = flow_id
|
16
|
+
self.response: FlowResponse = FlowResponse() if response is None else response
|
flowllm/context/registry.py
CHANGED
@@ -6,13 +6,17 @@ from flowllm.utils.common_utils import camel_to_snake
|
|
6
6
|
|
7
7
|
class Registry(BaseContext):
|
8
8
|
|
9
|
-
def __init__(self, registry_name: str, enable_log: bool = True, **kwargs):
|
9
|
+
def __init__(self, registry_name: str, enable_log: bool = True, register_flow_module: bool = True, **kwargs):
|
10
10
|
super().__init__(**kwargs)
|
11
11
|
self.registry_name: str = registry_name
|
12
12
|
self.enable_log: bool = enable_log
|
13
|
+
self.register_flow_module: bool = register_flow_module
|
13
14
|
|
14
15
|
def register(self, name: str = ""):
|
15
16
|
def decorator(cls):
|
17
|
+
if not self.register_flow_module and cls.__module__.startswith("flowllm"):
|
18
|
+
return cls
|
19
|
+
|
16
20
|
class_name = name if name else camel_to_snake(cls.__name__)
|
17
21
|
if self.enable_log:
|
18
22
|
if class_name in self._data:
|
@@ -1,9 +1,15 @@
|
|
1
|
+
import os
|
1
2
|
import uuid
|
2
3
|
from concurrent.futures import ThreadPoolExecutor
|
3
|
-
from
|
4
|
+
from inspect import isclass
|
5
|
+
from typing import Dict, List
|
6
|
+
|
7
|
+
import ray
|
8
|
+
from loguru import logger
|
4
9
|
|
5
10
|
from flowllm.context.base_context import BaseContext
|
6
11
|
from flowllm.context.registry import Registry
|
12
|
+
from flowllm.schema.service_config import ServiceConfig, EmbeddingModelConfig
|
7
13
|
from flowllm.utils.singleton import singleton
|
8
14
|
|
9
15
|
|
@@ -12,41 +18,82 @@ class ServiceContext(BaseContext):
|
|
12
18
|
|
13
19
|
def __init__(self, service_id: str = uuid.uuid4().hex, **kwargs):
|
14
20
|
super().__init__(**kwargs)
|
15
|
-
self.service_id: str = service_id
|
16
|
-
self.registry_dict: Dict[str, Registry] = \
|
17
|
-
{k: Registry(k) for k in ["embedding_model", "llm", "vector_store", "op", "flow_engine", "service"]}
|
18
|
-
|
19
|
-
@property
|
20
|
-
def language(self) -> str:
|
21
|
-
return self._data.get("language", "")
|
22
|
-
|
23
|
-
@language.setter
|
24
|
-
def language(self, value: str):
|
25
|
-
self._data["language"] = value
|
26
|
-
|
27
|
-
@property
|
28
|
-
def thread_pool(self) -> ThreadPoolExecutor:
|
29
|
-
return self._data["thread_pool"]
|
30
21
|
|
31
|
-
|
32
|
-
|
33
|
-
self.
|
22
|
+
self.service_id: str = service_id
|
23
|
+
self.service_config: ServiceConfig | None = None
|
24
|
+
self.language: str = ""
|
25
|
+
self.thread_pool: ThreadPoolExecutor | None = None
|
26
|
+
self.vector_store_dict: dict = {}
|
27
|
+
|
28
|
+
self.registry_dict: Dict[str, Registry] = {}
|
29
|
+
use_framework: bool = os.environ.get("FLOW_USE_FRAMEWORK", "").lower() == "true"
|
30
|
+
for key in ["embedding_model", "llm", "vector_store", "op", "tool_flow", "service"]:
|
31
|
+
enable_log = True
|
32
|
+
register_flow_module = True
|
33
|
+
|
34
|
+
if use_framework:
|
35
|
+
enable_log = False
|
36
|
+
if key in ["op", "tool_flow"]:
|
37
|
+
register_flow_module = False
|
38
|
+
self.registry_dict[key] = Registry(key, enable_log=enable_log, register_flow_module=register_flow_module)
|
39
|
+
|
40
|
+
self.tool_flow_dict: dict = {}
|
41
|
+
|
42
|
+
def set_default_service_config(self, parser=None):
|
43
|
+
if parser is None:
|
44
|
+
from flowllm.config.pydantic_config_parser import PydanticConfigParser
|
45
|
+
parser = PydanticConfigParser
|
46
|
+
|
47
|
+
config_parser = parser(ServiceConfig)
|
48
|
+
self.service_config = config_parser.parse_args("config=default")
|
49
|
+
return self
|
50
|
+
|
51
|
+
def init_by_service_config(self, service_config: ServiceConfig = None):
|
52
|
+
if service_config:
|
53
|
+
self.service_config = service_config
|
54
|
+
|
55
|
+
self.language = self.service_config.language
|
56
|
+
self.thread_pool = ThreadPoolExecutor(max_workers=self.service_config.thread_pool_max_workers)
|
57
|
+
if self.service_config.ray_max_workers > 1:
|
58
|
+
ray.init(num_cpus=self.service_config.ray_max_workers)
|
59
|
+
|
60
|
+
# add vector store
|
61
|
+
for name, config in self.service_config.vector_store.items():
|
62
|
+
vector_store_cls = self.resolve_vector_store(config.backend)
|
63
|
+
embedding_model_config: EmbeddingModelConfig = self.service_config.embedding_model[config.embedding_model]
|
64
|
+
embedding_model_cls = self.resolve_embedding_model(embedding_model_config.backend)
|
65
|
+
embedding_model = embedding_model_cls(model_name=embedding_model_config.model_name,
|
66
|
+
**embedding_model_config.params)
|
67
|
+
self.vector_store_dict[name] = vector_store_cls(embedding_model=embedding_model, **config.params)
|
68
|
+
|
69
|
+
from flowllm.flow.base_tool_flow import BaseToolFlow
|
70
|
+
from flowllm.flow.gallery import ExpressionToolFlow
|
71
|
+
|
72
|
+
# add tool flow cls
|
73
|
+
for name, tool_flow_cls in self.registry_dict["tool_flow"].items():
|
74
|
+
if not isclass(tool_flow_cls):
|
75
|
+
continue
|
76
|
+
|
77
|
+
tool_flow: BaseToolFlow = tool_flow_cls()
|
78
|
+
self.tool_flow_dict[tool_flow.name] = tool_flow
|
79
|
+
logger.info(f"add diy tool_flow: {tool_flow.name}")
|
80
|
+
|
81
|
+
# add tool flow config
|
82
|
+
for name, flow_config in self.service_config.flow.items():
|
83
|
+
flow_config.name = name
|
84
|
+
tool_flow: BaseToolFlow = ExpressionToolFlow(flow_config=flow_config)
|
85
|
+
self.tool_flow_dict[tool_flow.name] = tool_flow
|
86
|
+
logger.info(f"add expression tool_flow:{tool_flow.name}")
|
34
87
|
|
35
88
|
def get_vector_store(self, name: str = "default"):
|
36
|
-
|
37
|
-
if name not in vector_store_dict:
|
38
|
-
raise KeyError(f"vector store {name} not found")
|
39
|
-
|
40
|
-
return vector_store_dict[name]
|
89
|
+
return self.vector_store_dict[name]
|
41
90
|
|
42
|
-
def
|
43
|
-
|
44
|
-
self.set_vector_stores({})
|
91
|
+
def get_tool_flow(self, name: str = "default"):
|
92
|
+
return self.tool_flow_dict[name]
|
45
93
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
self._data["vector_store_dict"] = vector_store_dict
|
94
|
+
@property
|
95
|
+
def tool_flow_names(self) -> List[str]:
|
96
|
+
return sorted(self.tool_flow_dict.keys())
|
50
97
|
|
51
98
|
"""
|
52
99
|
register models
|
@@ -64,8 +111,8 @@ class ServiceContext(BaseContext):
|
|
64
111
|
def register_op(self, name: str = ""):
|
65
112
|
return self.registry_dict["op"].register(name=name)
|
66
113
|
|
67
|
-
def
|
68
|
-
return self.registry_dict["
|
114
|
+
def register_tool_flow(self, name: str = ""):
|
115
|
+
return self.registry_dict["tool_flow"].register(name=name)
|
69
116
|
|
70
117
|
def register_service(self, name: str = ""):
|
71
118
|
return self.registry_dict["service"].register(name=name)
|
@@ -90,14 +137,13 @@ class ServiceContext(BaseContext):
|
|
90
137
|
assert name in self.registry_dict["op"], f"op={name} not found!"
|
91
138
|
return self.registry_dict["op"][name]
|
92
139
|
|
93
|
-
def
|
94
|
-
assert name in self.registry_dict["
|
95
|
-
return self.registry_dict["
|
140
|
+
def resolve_tool_flow(self, name: str):
|
141
|
+
assert name in self.registry_dict["tool_flow"], f"tool_flow={name} not found!"
|
142
|
+
return self.registry_dict["tool_flow"][name]
|
96
143
|
|
97
144
|
def resolve_service(self, name: str):
|
98
145
|
assert name in self.registry_dict["service"], f"service={name} not found!"
|
99
146
|
return self.registry_dict["service"][name]
|
100
147
|
|
101
148
|
|
102
|
-
|
103
149
|
C = ServiceContext()
|
@@ -1 +1 @@
|
|
1
|
-
from
|
1
|
+
from .openai_compatible_embedding_model import OpenAICompatibleEmbeddingModel
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import asyncio
|
1
2
|
from abc import ABC
|
2
3
|
from typing import List
|
3
4
|
|
@@ -37,6 +38,21 @@ class BaseEmbeddingModel(BaseModel, ABC):
|
|
37
38
|
"""
|
38
39
|
raise NotImplementedError
|
39
40
|
|
41
|
+
async def _get_embeddings_async(self, input_text: str | List[str]):
|
42
|
+
"""
|
43
|
+
Abstract async method to get embeddings from the model.
|
44
|
+
|
45
|
+
This method must be implemented by concrete subclasses to provide
|
46
|
+
the actual async embedding functionality.
|
47
|
+
|
48
|
+
Args:
|
49
|
+
input_text: Single text string or list of text strings to embed
|
50
|
+
|
51
|
+
Returns:
|
52
|
+
Embedding vector(s) corresponding to the input text(s)
|
53
|
+
"""
|
54
|
+
raise NotImplementedError
|
55
|
+
|
40
56
|
def get_embeddings(self, input_text: str | List[str]):
|
41
57
|
"""
|
42
58
|
Get embeddings with retry logic and error handling.
|
@@ -64,6 +80,33 @@ class BaseEmbeddingModel(BaseModel, ABC):
|
|
64
80
|
# Return None if all retries failed and raise_exception is False
|
65
81
|
return None
|
66
82
|
|
83
|
+
async def get_embeddings_async(self, input_text: str | List[str]):
|
84
|
+
"""
|
85
|
+
Get embeddings asynchronously with retry logic and error handling.
|
86
|
+
|
87
|
+
This method wraps the _get_embeddings_async method with automatic retry
|
88
|
+
functionality in case of failures.
|
89
|
+
|
90
|
+
Args:
|
91
|
+
input_text: Single text string or list of text strings to embed
|
92
|
+
|
93
|
+
Returns:
|
94
|
+
Embedding vector(s) or None if all retries failed and raise_exception is False
|
95
|
+
"""
|
96
|
+
# Retry loop with exponential backoff potential
|
97
|
+
for i in range(self.max_retries):
|
98
|
+
try:
|
99
|
+
return await self._get_embeddings_async(input_text)
|
100
|
+
|
101
|
+
except Exception as e:
|
102
|
+
logger.exception(f"embedding model name={self.model_name} encounter error with e={e.args}")
|
103
|
+
# If this is the last retry and raise_exception is True, re-raise the exception
|
104
|
+
if i == self.max_retries - 1 and self.raise_exception:
|
105
|
+
raise e
|
106
|
+
|
107
|
+
# Return None if all retries failed and raise_exception is False
|
108
|
+
return None
|
109
|
+
|
67
110
|
def get_node_embeddings(self, nodes: VectorNode | List[VectorNode]):
|
68
111
|
"""
|
69
112
|
Generate embeddings for VectorNode objects and update their vector fields.
|
@@ -102,3 +145,51 @@ class BaseEmbeddingModel(BaseModel, ABC):
|
|
102
145
|
|
103
146
|
else:
|
104
147
|
raise TypeError(f"unsupported type={type(nodes)}")
|
148
|
+
|
149
|
+
async def get_node_embeddings_async(self, nodes: VectorNode | List[VectorNode]):
|
150
|
+
"""
|
151
|
+
Generate embeddings asynchronously for VectorNode objects and update their vector fields.
|
152
|
+
|
153
|
+
This method handles both single nodes and lists of nodes, with automatic
|
154
|
+
batching for efficient processing of large node lists.
|
155
|
+
|
156
|
+
Args:
|
157
|
+
nodes: Single VectorNode or list of VectorNode objects to embed
|
158
|
+
|
159
|
+
Returns:
|
160
|
+
The same node(s) with updated vector fields containing embeddings
|
161
|
+
|
162
|
+
Raises:
|
163
|
+
RuntimeError: If unsupported node type is provided
|
164
|
+
"""
|
165
|
+
# Handle single VectorNode
|
166
|
+
if isinstance(nodes, VectorNode):
|
167
|
+
nodes.vector = await self.get_embeddings_async(nodes.content)
|
168
|
+
return nodes
|
169
|
+
|
170
|
+
# Handle list of VectorNodes with batch processing
|
171
|
+
elif isinstance(nodes, list):
|
172
|
+
# Process nodes in batches to respect max_batch_size limits
|
173
|
+
batch_tasks = []
|
174
|
+
for i in range(0, len(nodes), self.max_batch_size):
|
175
|
+
batch_nodes = nodes[i:i + self.max_batch_size]
|
176
|
+
batch_content = [node.content for node in batch_nodes]
|
177
|
+
batch_tasks.append(self.get_embeddings_async(batch_content))
|
178
|
+
|
179
|
+
# Execute all batch tasks concurrently
|
180
|
+
batch_results = await asyncio.gather(*batch_tasks)
|
181
|
+
|
182
|
+
# Flatten the results
|
183
|
+
embeddings = [emb for batch_result in batch_results for emb in batch_result]
|
184
|
+
|
185
|
+
# Validate that we got the expected number of embeddings
|
186
|
+
if len(embeddings) != len(nodes):
|
187
|
+
logger.warning(f"embeddings.size={len(embeddings)} <> nodes.size={len(nodes)}")
|
188
|
+
else:
|
189
|
+
# Assign embeddings to corresponding nodes
|
190
|
+
for node, embedding in zip(nodes, embeddings):
|
191
|
+
node.vector = embedding
|
192
|
+
return nodes
|
193
|
+
|
194
|
+
else:
|
195
|
+
raise TypeError(f"unsupported type={type(nodes)}")
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import os
|
2
2
|
from typing import Literal, List
|
3
3
|
|
4
|
-
from openai import OpenAI
|
4
|
+
from openai import OpenAI, AsyncOpenAI
|
5
5
|
from pydantic import Field, PrivateAttr, model_validator
|
6
6
|
|
7
7
|
from flowllm.context.service_context import C
|
@@ -26,21 +26,23 @@ class OpenAICompatibleEmbeddingModel(BaseEmbeddingModel):
|
|
26
26
|
dimensions: int = Field(default=1024, description="Dimensionality of the embedding vectors")
|
27
27
|
encoding_format: Literal["float", "base64"] = Field(default="float", description="Encoding format for embeddings")
|
28
28
|
|
29
|
-
# Private OpenAI client
|
29
|
+
# Private OpenAI client instances
|
30
30
|
_client: OpenAI = PrivateAttr()
|
31
|
+
_async_client: AsyncOpenAI = PrivateAttr()
|
31
32
|
|
32
33
|
@model_validator(mode="after")
|
33
34
|
def init_client(self):
|
34
35
|
"""
|
35
|
-
Initialize the OpenAI
|
36
|
+
Initialize the OpenAI clients after model validation.
|
36
37
|
|
37
38
|
This method is called automatically after Pydantic model validation
|
38
|
-
to set up
|
39
|
+
to set up both sync and async OpenAI clients with the provided API key and base URL.
|
39
40
|
|
40
41
|
Returns:
|
41
42
|
self: The model instance for method chaining
|
42
43
|
"""
|
43
44
|
self._client = OpenAI(api_key=self.api_key, base_url=self.base_url)
|
45
|
+
self._async_client = AsyncOpenAI(api_key=self.api_key, base_url=self.base_url)
|
44
46
|
return self
|
45
47
|
|
46
48
|
def _get_embeddings(self, input_text: str | List[str]):
|
@@ -78,6 +80,41 @@ class OpenAICompatibleEmbeddingModel(BaseEmbeddingModel):
|
|
78
80
|
else:
|
79
81
|
raise RuntimeError(f"unsupported type={type(input_text)}")
|
80
82
|
|
83
|
+
async def _get_embeddings_async(self, input_text: str | List[str]):
|
84
|
+
"""
|
85
|
+
Get embeddings asynchronously from the OpenAI-compatible API.
|
86
|
+
|
87
|
+
This method implements the abstract _get_embeddings_async method from BaseEmbeddingModel
|
88
|
+
by calling the OpenAI-compatible embeddings API asynchronously.
|
89
|
+
|
90
|
+
Args:
|
91
|
+
input_text: Single text string or list of text strings to embed
|
92
|
+
|
93
|
+
Returns:
|
94
|
+
Embedding vector(s) corresponding to the input text(s)
|
95
|
+
|
96
|
+
Raises:
|
97
|
+
RuntimeError: If unsupported input type is provided
|
98
|
+
"""
|
99
|
+
completion = await self._async_client.embeddings.create(
|
100
|
+
model=self.model_name,
|
101
|
+
input=input_text,
|
102
|
+
dimensions=self.dimensions,
|
103
|
+
encoding_format=self.encoding_format
|
104
|
+
)
|
105
|
+
|
106
|
+
if isinstance(input_text, str):
|
107
|
+
return completion.data[0].embedding
|
108
|
+
|
109
|
+
elif isinstance(input_text, list):
|
110
|
+
result_emb = [[] for _ in range(len(input_text))]
|
111
|
+
for emb in completion.data:
|
112
|
+
result_emb[emb.index] = emb.embedding
|
113
|
+
return result_emb
|
114
|
+
|
115
|
+
else:
|
116
|
+
raise RuntimeError(f"unsupported type={type(input_text)}")
|
117
|
+
|
81
118
|
|
82
119
|
def main():
|
83
120
|
from flowllm.utils.common_utils import load_env
|
@@ -91,5 +128,26 @@ def main():
|
|
91
128
|
print(res2)
|
92
129
|
|
93
130
|
|
131
|
+
async def async_main():
|
132
|
+
from flowllm.utils.common_utils import load_env
|
133
|
+
|
134
|
+
load_env()
|
135
|
+
model = OpenAICompatibleEmbeddingModel(dimensions=64, model_name="text-embedding-v4")
|
136
|
+
|
137
|
+
# Test async single text embedding
|
138
|
+
res1 = await model.get_embeddings_async(
|
139
|
+
"The clothes are of good quality and look good, definitely worth the wait. I love them.")
|
140
|
+
|
141
|
+
# Test async batch text embedding
|
142
|
+
res2 = await model.get_embeddings_async(["aa", "bb"])
|
143
|
+
|
144
|
+
print("Async results:")
|
145
|
+
print(res1)
|
146
|
+
print(res2)
|
147
|
+
|
148
|
+
|
94
149
|
if __name__ == "__main__":
|
95
|
-
main()
|
150
|
+
# main()
|
151
|
+
import asyncio
|
152
|
+
|
153
|
+
asyncio.run(async_main())
|
flowllm/flow/__init__.py
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
from . import gallery
|
@@ -0,0 +1,74 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
from typing import Optional
|
3
|
+
|
4
|
+
from loguru import logger
|
5
|
+
|
6
|
+
from flowllm.context.flow_context import FlowContext
|
7
|
+
from flowllm.op.base_op import BaseOp
|
8
|
+
from flowllm.op.parallel_op import ParallelOp
|
9
|
+
from flowllm.op.sequential_op import SequentialOp
|
10
|
+
from flowllm.schema.flow_response import FlowResponse
|
11
|
+
from flowllm.utils.common_utils import camel_to_snake
|
12
|
+
|
13
|
+
|
14
|
+
class BaseFlow(ABC):
|
15
|
+
|
16
|
+
def __init__(self, name: str = "", **kwargs):
|
17
|
+
self.name: str = name or camel_to_snake(self.__class__.__name__)
|
18
|
+
self.flow_params: dict = kwargs
|
19
|
+
|
20
|
+
self.flow_op: BaseOp = self.build_flow()
|
21
|
+
self.print_flow()
|
22
|
+
|
23
|
+
@abstractmethod
|
24
|
+
def build_flow(self):
|
25
|
+
...
|
26
|
+
|
27
|
+
def print_flow(self):
|
28
|
+
assert self.flow_op is not None, "flow_content is not parsed!"
|
29
|
+
logger.info(f"---------- start print flow={self.name} ----------")
|
30
|
+
self._print_operation_tree(self.flow_op, indent=0)
|
31
|
+
logger.info(f"---------- end print flow={self.name} ----------")
|
32
|
+
|
33
|
+
def _print_operation_tree(self, op: BaseOp, indent: int):
|
34
|
+
"""
|
35
|
+
Recursively print the operation tree structure.
|
36
|
+
|
37
|
+
Args:
|
38
|
+
op: The operation to print
|
39
|
+
indent: Current indentation level
|
40
|
+
"""
|
41
|
+
prefix = " " * indent
|
42
|
+
if isinstance(op, SequentialOp):
|
43
|
+
logger.info(f"{prefix}Sequential Execution:")
|
44
|
+
for i, sub_op in enumerate(op.ops):
|
45
|
+
logger.info(f"{prefix} Step {i + 1}:")
|
46
|
+
self._print_operation_tree(sub_op, indent + 2)
|
47
|
+
|
48
|
+
elif isinstance(op, ParallelOp):
|
49
|
+
logger.info(f"{prefix}Parallel Execution:")
|
50
|
+
for i, sub_op in enumerate(op.ops):
|
51
|
+
logger.info(f"{prefix} Branch {i + 1}:")
|
52
|
+
self._print_operation_tree(sub_op, indent + 2)
|
53
|
+
|
54
|
+
else:
|
55
|
+
logger.info(f"{prefix}Operation: {op.name}")
|
56
|
+
|
57
|
+
def return_callback(self, context: FlowContext):
|
58
|
+
logger.info(f"context.response={context.response.model_dump_json()}")
|
59
|
+
return context.response
|
60
|
+
|
61
|
+
def __call__(self, **kwargs) -> FlowResponse:
|
62
|
+
context = FlowContext(**kwargs)
|
63
|
+
logger.info(f"request.params={kwargs}")
|
64
|
+
|
65
|
+
try:
|
66
|
+
flow_op = self.build_flow()
|
67
|
+
flow_op(context=context)
|
68
|
+
|
69
|
+
except Exception as e:
|
70
|
+
logger.exception(f"flow_name={self.name} encounter error={e.args}")
|
71
|
+
context.response.success = False
|
72
|
+
context.response.answer = str(e.args)
|
73
|
+
|
74
|
+
return self.return_callback(context=context)
|
@@ -0,0 +1,15 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
|
3
|
+
from flowllm.flow.base_flow import BaseFlow
|
4
|
+
from flowllm.schema.tool_call import ToolCall
|
5
|
+
|
6
|
+
|
7
|
+
class BaseToolFlow(BaseFlow, ABC):
|
8
|
+
|
9
|
+
def __init__(self, **kwargs):
|
10
|
+
super().__init__(**kwargs)
|
11
|
+
self.tool_call: ToolCall = self.build_tool_call()
|
12
|
+
|
13
|
+
@abstractmethod
|
14
|
+
def build_tool_call(self) -> ToolCall:
|
15
|
+
...
|
@@ -0,0 +1,8 @@
|
|
1
|
+
from .cmd_flow import CmdFlow
|
2
|
+
from .code_tool_flow import CodeToolFlow
|
3
|
+
from .dashscope_search_tool_flow import DashscopeSearchToolFlow
|
4
|
+
from .deepsearch_tool_flow import DeepSearchToolFlow
|
5
|
+
from .expression_tool_flow import ExpressionToolFlow
|
6
|
+
from .mock_tool_flow import MockToolFlow
|
7
|
+
from .tavily_search_tool_flow import TavilySearchToolFlow
|
8
|
+
from .terminate_tool_flow import TerminateToolFlow
|
@@ -0,0 +1,11 @@
|
|
1
|
+
from flowllm.flow.base_flow import BaseFlow
|
2
|
+
from flowllm.flow.parser.expression_parser import ExpressionParser
|
3
|
+
|
4
|
+
|
5
|
+
class CmdFlow(BaseFlow):
|
6
|
+
|
7
|
+
def build_flow(self):
|
8
|
+
flow: str = self.flow_params["flow"]
|
9
|
+
assert flow, "add `flow=<op_flow>` in cmd!"
|
10
|
+
parser = ExpressionParser(flow)
|
11
|
+
return parser.parse_flow()
|
@@ -0,0 +1,30 @@
|
|
1
|
+
from flowllm.context.flow_context import FlowContext
|
2
|
+
from flowllm.context.service_context import C
|
3
|
+
from flowllm.flow.base_tool_flow import BaseToolFlow
|
4
|
+
from flowllm.op.code.execute_code_op import ExecuteCodeOp
|
5
|
+
from flowllm.schema.tool_call import ToolCall
|
6
|
+
|
7
|
+
|
8
|
+
@C.register_tool_flow()
|
9
|
+
class CodeToolFlow(BaseToolFlow):
|
10
|
+
|
11
|
+
def build_flow(self):
|
12
|
+
return ExecuteCodeOp()
|
13
|
+
|
14
|
+
def build_tool_call(self) -> ToolCall:
|
15
|
+
return ToolCall(**{
|
16
|
+
"name": "python_execute",
|
17
|
+
"description": "Execute python code can be used in scenarios such as analysis or calculation, and the final result can be printed using the `print` function.",
|
18
|
+
"input_schema": {
|
19
|
+
"code": {
|
20
|
+
"type": "str",
|
21
|
+
"description": "code to be executed. Please do not execute any matplotlib code here.",
|
22
|
+
"required": True
|
23
|
+
}
|
24
|
+
}
|
25
|
+
})
|
26
|
+
|
27
|
+
def return_callback(self, context: FlowContext):
|
28
|
+
context.response.answer = context.code_result
|
29
|
+
return context.response
|
30
|
+
|
@@ -0,0 +1,34 @@
|
|
1
|
+
from flowllm.context.flow_context import FlowContext
|
2
|
+
from flowllm.context.service_context import C
|
3
|
+
from flowllm.flow.base_tool_flow import BaseToolFlow
|
4
|
+
from flowllm.op.search import DashscopeSearchOp
|
5
|
+
from flowllm.schema.tool_call import ToolCall
|
6
|
+
|
7
|
+
|
8
|
+
@C.register_tool_flow()
|
9
|
+
class DashscopeSearchToolFlow(BaseToolFlow):
|
10
|
+
|
11
|
+
def build_flow(self):
|
12
|
+
return DashscopeSearchOp()
|
13
|
+
|
14
|
+
def build_tool_call(self) -> ToolCall:
|
15
|
+
return ToolCall(**{
|
16
|
+
"name": "web_search",
|
17
|
+
"description": "Use search keywords to retrieve relevant information from the internet. If there are multiple search keywords, please use each keyword separately to call this tool.",
|
18
|
+
"input_schema": {
|
19
|
+
"query": {
|
20
|
+
"type": "str",
|
21
|
+
"description": "search keyword",
|
22
|
+
"required": True
|
23
|
+
}
|
24
|
+
}
|
25
|
+
})
|
26
|
+
|
27
|
+
def return_callback(self, context: FlowContext):
|
28
|
+
context.response.answer = context.dashscope_search_result
|
29
|
+
return context.response
|
30
|
+
|
31
|
+
|
32
|
+
if __name__ == "__main__":
|
33
|
+
flow = DashscopeSearchToolFlow()
|
34
|
+
flow(query="what is AI?")
|