flowllm 0.1.0__py3-none-any.whl → 0.1.1__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 +12 -0
- flowllm/app.py +25 -0
- flowllm/config/default_config.yaml +82 -0
- flowllm/config/pydantic_config_parser.py +242 -0
- flowllm/context/base_context.py +59 -0
- flowllm/context/flow_context.py +28 -0
- llmflow/op/prompt_mixin.py → flowllm/context/prompt_handler.py +25 -14
- flowllm/context/registry.py +26 -0
- flowllm/context/service_context.py +103 -0
- flowllm/embedding_model/__init__.py +1 -0
- {llmflow → flowllm}/embedding_model/base_embedding_model.py +2 -2
- {llmflow → flowllm}/embedding_model/openai_compatible_embedding_model.py +8 -8
- flowllm/flow_engine/__init__.py +1 -0
- flowllm/flow_engine/base_flow_engine.py +34 -0
- flowllm/flow_engine/simple_flow_engine.py +213 -0
- flowllm/llm/__init__.py +1 -0
- {llmflow → flowllm}/llm/base_llm.py +16 -24
- {llmflow → flowllm}/llm/openai_compatible_llm.py +64 -108
- flowllm/op/__init__.py +3 -0
- flowllm/op/akshare/get_ak_a_code_op.py +116 -0
- flowllm/op/akshare/get_ak_a_code_prompt.yaml +21 -0
- flowllm/op/akshare/get_ak_a_info_op.py +143 -0
- flowllm/op/base_op.py +169 -0
- flowllm/op/llm_base_op.py +63 -0
- flowllm/op/mock_op.py +42 -0
- flowllm/op/parallel_op.py +30 -0
- flowllm/op/sequential_op.py +29 -0
- flowllm/schema/flow_response.py +12 -0
- flowllm/schema/message.py +35 -0
- flowllm/schema/service_config.py +76 -0
- flowllm/schema/tool_call.py +110 -0
- flowllm/service/__init__.py +2 -0
- flowllm/service/base_service.py +59 -0
- flowllm/service/http_service.py +87 -0
- flowllm/service/mcp_service.py +45 -0
- flowllm/storage/__init__.py +1 -0
- flowllm/storage/vector_store/__init__.py +3 -0
- flowllm/storage/vector_store/base_vector_store.py +44 -0
- {llmflow → flowllm/storage}/vector_store/chroma_vector_store.py +11 -10
- {llmflow → flowllm/storage}/vector_store/es_vector_store.py +10 -9
- llmflow/vector_store/file_vector_store.py → flowllm/storage/vector_store/local_vector_store.py +110 -10
- flowllm/utils/common_utils.py +64 -0
- flowllm/utils/dataframe_cache.py +331 -0
- flowllm/utils/fetch_url.py +113 -0
- {llmflow → flowllm}/utils/timer.py +5 -4
- {flowllm-0.1.0.dist-info → flowllm-0.1.1.dist-info}/METADATA +31 -27
- flowllm-0.1.1.dist-info/RECORD +62 -0
- flowllm-0.1.1.dist-info/entry_points.txt +4 -0
- {flowllm-0.1.0.dist-info → flowllm-0.1.1.dist-info}/licenses/LICENSE +1 -1
- flowllm-0.1.1.dist-info/top_level.txt +1 -0
- flowllm-0.1.0.dist-info/RECORD +0 -66
- flowllm-0.1.0.dist-info/entry_points.txt +0 -3
- flowllm-0.1.0.dist-info/top_level.txt +0 -1
- llmflow/app.py +0 -53
- llmflow/config/config_parser.py +0 -80
- llmflow/config/mock_config.yaml +0 -58
- llmflow/embedding_model/__init__.py +0 -5
- llmflow/enumeration/agent_state.py +0 -8
- llmflow/llm/__init__.py +0 -5
- llmflow/mcp_server.py +0 -110
- llmflow/op/__init__.py +0 -10
- llmflow/op/base_op.py +0 -125
- llmflow/op/mock_op.py +0 -40
- llmflow/op/react/react_v1_op.py +0 -88
- llmflow/op/react/react_v1_prompt.yaml +0 -28
- llmflow/op/vector_store/__init__.py +0 -13
- llmflow/op/vector_store/recall_vector_store_op.py +0 -48
- llmflow/op/vector_store/update_vector_store_op.py +0 -28
- llmflow/op/vector_store/vector_store_action_op.py +0 -46
- llmflow/pipeline/pipeline.py +0 -94
- llmflow/pipeline/pipeline_context.py +0 -37
- llmflow/schema/app_config.py +0 -69
- llmflow/schema/experience.py +0 -144
- llmflow/schema/message.py +0 -68
- llmflow/schema/request.py +0 -32
- llmflow/schema/response.py +0 -29
- llmflow/service/__init__.py +0 -0
- llmflow/service/llmflow_service.py +0 -96
- llmflow/tool/__init__.py +0 -9
- llmflow/tool/base_tool.py +0 -80
- llmflow/tool/code_tool.py +0 -43
- llmflow/tool/dashscope_search_tool.py +0 -162
- llmflow/tool/mcp_tool.py +0 -77
- llmflow/tool/tavily_search_tool.py +0 -109
- llmflow/tool/terminate_tool.py +0 -23
- llmflow/utils/__init__.py +0 -0
- llmflow/utils/common_utils.py +0 -17
- llmflow/utils/file_handler.py +0 -25
- llmflow/utils/http_client.py +0 -156
- llmflow/utils/op_utils.py +0 -102
- llmflow/utils/registry.py +0 -33
- llmflow/vector_store/__init__.py +0 -7
- llmflow/vector_store/base_vector_store.py +0 -136
- {llmflow → flowllm/config}/__init__.py +0 -0
- {llmflow/config → flowllm/context}/__init__.py +0 -0
- {llmflow → flowllm}/enumeration/__init__.py +0 -0
- {llmflow → flowllm}/enumeration/chunk_enum.py +0 -0
- {llmflow → flowllm}/enumeration/http_enum.py +0 -0
- {llmflow → flowllm}/enumeration/role.py +0 -0
- {llmflow/op/react → flowllm/op/akshare}/__init__.py +0 -0
- {llmflow/pipeline → flowllm/schema}/__init__.py +0 -0
- {llmflow → flowllm}/schema/vector_node.py +0 -0
- {llmflow/schema → flowllm/utils}/__init__.py +0 -0
- {llmflow → flowllm}/utils/singleton.py +0 -0
- {flowllm-0.1.0.dist-info → flowllm-0.1.1.dist-info}/WHEEL +0 -0
@@ -0,0 +1,116 @@
|
|
1
|
+
import json
|
2
|
+
import time
|
3
|
+
from typing import List
|
4
|
+
|
5
|
+
import akshare as ak
|
6
|
+
import pandas as pd
|
7
|
+
from loguru import logger
|
8
|
+
|
9
|
+
from flowllm.config.pydantic_config_parser import get_default_config
|
10
|
+
from flowllm.context.flow_context import FlowContext
|
11
|
+
from flowllm.context.service_context import C
|
12
|
+
from flowllm.enumeration.role import Role
|
13
|
+
from flowllm.op.llm_base_op import BaseLLMOp
|
14
|
+
from flowllm.schema.message import Message
|
15
|
+
from flowllm.utils.dataframe_cache import DataFrameCache
|
16
|
+
from flowllm.utils.timer import timer
|
17
|
+
|
18
|
+
|
19
|
+
@C.register_op()
|
20
|
+
class GetAkACodeOp(BaseLLMOp):
|
21
|
+
file_path: str = __file__
|
22
|
+
|
23
|
+
def __init__(self, language: str = "zh", llm="qwen3_30b_instruct", **kwargs):
|
24
|
+
super().__init__(language=language, llm=llm, **kwargs)
|
25
|
+
|
26
|
+
@staticmethod
|
27
|
+
def download_a_stock_df():
|
28
|
+
df_cache = DataFrameCache()
|
29
|
+
save_df_key: str = "all_a_stock_name_code"
|
30
|
+
if not df_cache.exists(save_df_key):
|
31
|
+
stock_sh_a_spot_em_df = ak.stock_sh_a_spot_em()
|
32
|
+
stock_sz_a_spot_em_df = ak.stock_sz_a_spot_em()
|
33
|
+
stock_bj_a_spot_em_df = ak.stock_bj_a_spot_em()
|
34
|
+
|
35
|
+
df: pd.DataFrame = pd.concat([stock_sh_a_spot_em_df, stock_sz_a_spot_em_df, stock_bj_a_spot_em_df], axis=0)
|
36
|
+
df = df.drop(columns=["序号"])
|
37
|
+
df = df.reset_index(drop=True)
|
38
|
+
df = df.sort_values(by="代码")
|
39
|
+
df_cache.save(save_df_key, df, expire_hours=0.25)
|
40
|
+
|
41
|
+
df = df_cache.load(save_df_key, dtype={"代码": str})
|
42
|
+
return df
|
43
|
+
|
44
|
+
def get_name_code_dict(self) -> dict:
|
45
|
+
df = self.download_a_stock_df()
|
46
|
+
|
47
|
+
name_code_dict = {}
|
48
|
+
for line in df.to_dict(orient="records"):
|
49
|
+
name = line["名称"].replace(" ", "")
|
50
|
+
code = line["代码"]
|
51
|
+
name_code_dict[name] = code
|
52
|
+
logger.info(f"name_code_dict.size={len(name_code_dict)} content={str(name_code_dict)[:50]}...")
|
53
|
+
return name_code_dict
|
54
|
+
|
55
|
+
@staticmethod
|
56
|
+
def split_list(array_list: list, n: int):
|
57
|
+
if n <= 0:
|
58
|
+
raise ValueError
|
59
|
+
|
60
|
+
length = len(array_list)
|
61
|
+
base_size = length // n
|
62
|
+
remainder = length % n
|
63
|
+
|
64
|
+
start = 0
|
65
|
+
for i in range(n):
|
66
|
+
size = base_size + (1 if i < remainder else 0)
|
67
|
+
end = start + size
|
68
|
+
yield array_list[start:end]
|
69
|
+
start = end
|
70
|
+
|
71
|
+
@timer()
|
72
|
+
def find_stock_codes(self, stock_names: List[str]):
|
73
|
+
stock_names = "\n".join([x.strip() for x in stock_names if x])
|
74
|
+
prompt = self.prompt_format(prompt_name="find_stock_name",
|
75
|
+
stock_names=stock_names,
|
76
|
+
query=self.flow_context.query)
|
77
|
+
logger.info(f"prompt={prompt}")
|
78
|
+
|
79
|
+
def callback_fn(msg: Message):
|
80
|
+
content = msg.content
|
81
|
+
if "```" in content:
|
82
|
+
content = content.split("```")[1]
|
83
|
+
content = content.strip("json")
|
84
|
+
content = json.loads(content.strip())
|
85
|
+
return content
|
86
|
+
|
87
|
+
codes: List[str] = self.llm.chat(messages=[Message(role=Role.USER, content=prompt)],
|
88
|
+
enable_stream_print=False,
|
89
|
+
callback_fn=callback_fn)
|
90
|
+
return codes
|
91
|
+
|
92
|
+
def execute(self):
|
93
|
+
name_code_dict = self.get_name_code_dict()
|
94
|
+
stock_names = list(name_code_dict.keys())
|
95
|
+
for p_stock_names in self.split_list(stock_names, n=2):
|
96
|
+
self.submit_task(self.find_stock_codes, stock_names=p_stock_names)
|
97
|
+
time.sleep(1)
|
98
|
+
|
99
|
+
stock_names = sorted(set(self.join_task()))
|
100
|
+
self.flow_context.code_infos = {name_code_dict[n]: {} for n in stock_names}
|
101
|
+
logger.info(f"code_infos={self.flow_context.code_infos}")
|
102
|
+
|
103
|
+
|
104
|
+
if __name__ == "__main__":
|
105
|
+
from concurrent.futures import ThreadPoolExecutor
|
106
|
+
|
107
|
+
C.thread_pool = ThreadPoolExecutor(max_workers=10)
|
108
|
+
flow_context = FlowContext()
|
109
|
+
service_config = get_default_config()
|
110
|
+
flow_context.query = "茅台和五粮现在价格多少?"
|
111
|
+
flow_context.service_config = service_config
|
112
|
+
|
113
|
+
op = GetAkACodeOp(flow_context=flow_context)
|
114
|
+
# for x in op.split_list(list(range(10)), 3):
|
115
|
+
# print(x)
|
116
|
+
op.execute()
|
@@ -0,0 +1,21 @@
|
|
1
|
+
find_stock_name_zh: |
|
2
|
+
# 股票标准名称
|
3
|
+
{stock_names}
|
4
|
+
|
5
|
+
# 用户问题
|
6
|
+
{query}
|
7
|
+
|
8
|
+
# 任务
|
9
|
+
请你提取出用户问题中提到的股票名称,并通过**股票标准名称**找到对应的标准名称,并以json格式返回。
|
10
|
+
如果用户问题中没有提及股票名称或者**股票标准名称**中没有找到,请返回空"[]"。
|
11
|
+
如果用户只是提到了某个行业,并且没有明确提及股票名称,请返回空"[]"。
|
12
|
+
请思考后输出你的答案。
|
13
|
+
|
14
|
+
# 答案格式
|
15
|
+
```json
|
16
|
+
[
|
17
|
+
"股票标准名称1",
|
18
|
+
"股票标准名称2",
|
19
|
+
...
|
20
|
+
]
|
21
|
+
```
|
@@ -0,0 +1,143 @@
|
|
1
|
+
import json
|
2
|
+
import time
|
3
|
+
|
4
|
+
import akshare as ak
|
5
|
+
import pandas as pd
|
6
|
+
from loguru import logger
|
7
|
+
|
8
|
+
from flowllm.config.pydantic_config_parser import get_default_config
|
9
|
+
from flowllm.context.flow_context import FlowContext
|
10
|
+
from flowllm.context.service_context import C
|
11
|
+
from flowllm.op.base_op import BaseOp
|
12
|
+
from flowllm.utils.fetch_url import fetch_webpage_text
|
13
|
+
|
14
|
+
|
15
|
+
@C.register_op()
|
16
|
+
class GetAkAInfoOp(BaseOp):
|
17
|
+
|
18
|
+
def execute_code(self, code: str) -> dict:
|
19
|
+
df = ak.stock_individual_info_em(symbol=code)
|
20
|
+
result = {}
|
21
|
+
for line in df.to_dict(orient="records"):
|
22
|
+
result[line["item"].strip()] = line["value"]
|
23
|
+
return {"基本信息": result}
|
24
|
+
|
25
|
+
def execute(self):
|
26
|
+
max_retries: int = self.op_params.get("max_retries", 3)
|
27
|
+
for code, info_dict in self.flow_context.code_infos.items():
|
28
|
+
result = {}
|
29
|
+
for i in range(max_retries):
|
30
|
+
try:
|
31
|
+
result = self.execute_code(code)
|
32
|
+
break
|
33
|
+
|
34
|
+
except Exception as _:
|
35
|
+
if i != max_retries - 1:
|
36
|
+
time.sleep(i * 2 + 1)
|
37
|
+
|
38
|
+
if result:
|
39
|
+
info_dict.update(result)
|
40
|
+
|
41
|
+
time.sleep(1)
|
42
|
+
logger.info(f"code_infos={json.dumps(self.flow_context.code_infos, ensure_ascii=False, indent=2)}")
|
43
|
+
|
44
|
+
|
45
|
+
@C.register_op()
|
46
|
+
class GetAkASpotOp(GetAkAInfoOp):
|
47
|
+
|
48
|
+
def execute_code(self, code: str) -> dict:
|
49
|
+
from flowllm.op import GetAkACodeOp
|
50
|
+
|
51
|
+
df: pd.DataFrame = GetAkACodeOp.download_a_stock_df()
|
52
|
+
df = df.loc[df["代码"] == code, :]
|
53
|
+
result = {}
|
54
|
+
if len(df) > 0:
|
55
|
+
result["实时行情"] = df.to_dict(orient="records")[-1]
|
56
|
+
|
57
|
+
return result
|
58
|
+
|
59
|
+
|
60
|
+
@C.register_op()
|
61
|
+
class GetAkAMoneyFlowOp(GetAkAInfoOp):
|
62
|
+
|
63
|
+
def execute_code(self, code: str) -> dict:
|
64
|
+
df = ak.stock_individual_fund_flow(stock=code)
|
65
|
+
result = {}
|
66
|
+
if len(df) > 0:
|
67
|
+
result["资金流入流出"] = {k: str(v) for k, v in df.to_dict(orient="records")[-1].items()}
|
68
|
+
return result
|
69
|
+
|
70
|
+
|
71
|
+
@C.register_op()
|
72
|
+
class GetAkAFinancialInfoOp(GetAkAInfoOp):
|
73
|
+
|
74
|
+
def execute_code(self, code: str) -> dict:
|
75
|
+
df = ak.stock_financial_abstract_ths(symbol=code, indicator="按报告期")
|
76
|
+
result = {}
|
77
|
+
if len(df) > 0:
|
78
|
+
result["财务信息"] = {k: str(v) for k, v in df.to_dict(orient="records")[-1].items()}
|
79
|
+
return result
|
80
|
+
|
81
|
+
|
82
|
+
@C.register_op()
|
83
|
+
class GetAkANewsOp(GetAkAInfoOp):
|
84
|
+
|
85
|
+
def execute_code(self, code: str) -> dict:
|
86
|
+
stock_news_em_df = ak.stock_news_em(symbol=code)
|
87
|
+
top_n_news: int = self.op_params.get("top_n_news", 1)
|
88
|
+
|
89
|
+
news_content_list = []
|
90
|
+
for i, line in enumerate(stock_news_em_df.to_dict(orient="records")[:top_n_news]):
|
91
|
+
url = line["新闻链接"]
|
92
|
+
# http://finance.eastmoney.com/a/202508133482756869.html
|
93
|
+
ts = url.split("/")[-1].split(".")[0]
|
94
|
+
date = ts[:8]
|
95
|
+
content = fetch_webpage_text(url).strip()
|
96
|
+
content = f"新闻{i}\n时间{date}\n{content}"
|
97
|
+
news_content_list.append(content)
|
98
|
+
|
99
|
+
return {"新闻": "\n\n".join(news_content_list)}
|
100
|
+
|
101
|
+
|
102
|
+
@C.register_op()
|
103
|
+
class MergeAkAInfoOp(BaseOp):
|
104
|
+
|
105
|
+
def execute(self):
|
106
|
+
code_content = {}
|
107
|
+
for code, info_dict in self.flow_context.code_infos.items():
|
108
|
+
content_list = [f"\n\n### {code}"]
|
109
|
+
for key, value in info_dict.items():
|
110
|
+
content_list.append(f"\n#### {code}-{key}")
|
111
|
+
if isinstance(value, str):
|
112
|
+
content_list.append(value)
|
113
|
+
elif isinstance(value, dict):
|
114
|
+
for attr_name, attr_value in value.items():
|
115
|
+
content_list.append(f"{attr_name}: {attr_value}")
|
116
|
+
elif isinstance(value, list):
|
117
|
+
content_list.extend([x.strip() for x in value if x])
|
118
|
+
|
119
|
+
code_content[code] = "\n".join(content_list)
|
120
|
+
|
121
|
+
answer = "\n".join(code_content.values())
|
122
|
+
logger.info(f"answer=\n{answer}")
|
123
|
+
self.flow_context.response.answer = answer.strip()
|
124
|
+
|
125
|
+
|
126
|
+
if __name__ == "__main__":
|
127
|
+
from flowllm.schema.flow_response import FlowResponse
|
128
|
+
|
129
|
+
code_infos = {"000858": {}, "600519": {}}
|
130
|
+
flow_context = FlowContext(code_infos=code_infos, response=FlowResponse())
|
131
|
+
service_config = get_default_config()
|
132
|
+
flow_context.query = "茅台和五粮现在价格多少?"
|
133
|
+
flow_context.service_config = service_config
|
134
|
+
|
135
|
+
op1 = GetAkAInfoOp(flow_context=flow_context)
|
136
|
+
op2 = GetAkASpotOp(flow_context=flow_context)
|
137
|
+
op3 = GetAkAMoneyFlowOp(flow_context=flow_context)
|
138
|
+
op4 = GetAkAFinancialInfoOp(flow_context=flow_context)
|
139
|
+
op5 = GetAkANewsOp(flow_context=flow_context)
|
140
|
+
op6 = MergeAkAInfoOp(flow_context=flow_context)
|
141
|
+
|
142
|
+
op = op1 >> op2 >> op3 >> op4 >> op5 >> op6
|
143
|
+
op.execute()
|
flowllm/op/base_op.py
ADDED
@@ -0,0 +1,169 @@
|
|
1
|
+
"""
|
2
|
+
BaseOp operator overloading implementation
|
3
|
+
|
4
|
+
Supported operators:
|
5
|
+
- op1 >> op2: Sequential execution, output of op1 becomes input of op2
|
6
|
+
- op1 | op2: Parallel execution, both operations use the same input, returns list of results
|
7
|
+
- Mixed calls: op1 >> (op2 | op3) >> op4
|
8
|
+
|
9
|
+
Usage examples:
|
10
|
+
# Sequential execution
|
11
|
+
result = op1 >> op2 >> op3
|
12
|
+
|
13
|
+
# Parallel execution
|
14
|
+
results = op1 | op2 | op3
|
15
|
+
|
16
|
+
# Mixed calls
|
17
|
+
result = op1 >> (op2 | op3) >> op4
|
18
|
+
result = op1 >> (op1 | (op2 >> op3)) >> op4
|
19
|
+
"""
|
20
|
+
|
21
|
+
from abc import abstractmethod, ABC
|
22
|
+
from concurrent.futures import Future
|
23
|
+
from typing import List
|
24
|
+
|
25
|
+
from loguru import logger
|
26
|
+
from tqdm import tqdm
|
27
|
+
|
28
|
+
from flowllm.context.flow_context import FlowContext
|
29
|
+
from flowllm.context.service_context import C
|
30
|
+
from flowllm.utils.common_utils import camel_to_snake
|
31
|
+
from flowllm.utils.timer import Timer
|
32
|
+
|
33
|
+
|
34
|
+
class BaseOp(ABC):
|
35
|
+
|
36
|
+
def __init__(self,
|
37
|
+
name: str = "",
|
38
|
+
language: str = "",
|
39
|
+
raise_exception: bool = True,
|
40
|
+
flow_context: FlowContext | None = None,
|
41
|
+
**kwargs):
|
42
|
+
|
43
|
+
super().__init__()
|
44
|
+
|
45
|
+
self.name: str = name or camel_to_snake(self.__class__.__name__)
|
46
|
+
self.language: str = language or C.language
|
47
|
+
self.raise_exception: bool = raise_exception
|
48
|
+
|
49
|
+
self.flow_context: FlowContext | None = flow_context
|
50
|
+
self.op_params: dict = kwargs
|
51
|
+
|
52
|
+
self.task_list: List[Future] = []
|
53
|
+
self.timer = Timer(name=self.name)
|
54
|
+
|
55
|
+
@abstractmethod
|
56
|
+
def execute(self):
|
57
|
+
...
|
58
|
+
|
59
|
+
def __call__(self, *args, **kwargs):
|
60
|
+
with self.timer:
|
61
|
+
if self.raise_exception:
|
62
|
+
return self.execute()
|
63
|
+
|
64
|
+
else:
|
65
|
+
try:
|
66
|
+
return self.execute()
|
67
|
+
|
68
|
+
except Exception as e:
|
69
|
+
logger.exception(f"op={self.name} execute failed, error={e.args}")
|
70
|
+
|
71
|
+
def submit_task(self, fn, *args, **kwargs):
|
72
|
+
task = C.thread_pool.submit(fn, *args, **kwargs)
|
73
|
+
self.task_list.append(task)
|
74
|
+
return self
|
75
|
+
|
76
|
+
def join_task(self, task_desc: str = None) -> list:
|
77
|
+
result = []
|
78
|
+
for task in tqdm(self.task_list, desc=task_desc or self.name):
|
79
|
+
t_result = task.result()
|
80
|
+
if t_result:
|
81
|
+
if isinstance(t_result, list):
|
82
|
+
result.extend(t_result)
|
83
|
+
else:
|
84
|
+
result.append(t_result)
|
85
|
+
self.task_list.clear()
|
86
|
+
return result
|
87
|
+
|
88
|
+
def __rshift__(self, op: "BaseOp"):
|
89
|
+
from flowllm.op.sequential_op import SequentialOp
|
90
|
+
|
91
|
+
sequential_op = SequentialOp(ops=[self], flow_context=self.flow_context)
|
92
|
+
|
93
|
+
if isinstance(op, SequentialOp):
|
94
|
+
sequential_op.ops.extend(op.ops)
|
95
|
+
else:
|
96
|
+
sequential_op.ops.append(op)
|
97
|
+
return sequential_op
|
98
|
+
|
99
|
+
def __or__(self, op: "BaseOp"):
|
100
|
+
from flowllm.op.parallel_op import ParallelOp
|
101
|
+
|
102
|
+
parallel_op = ParallelOp(ops=[self], flow_context=self.flow_context)
|
103
|
+
|
104
|
+
if isinstance(op, ParallelOp):
|
105
|
+
parallel_op.ops.extend(op.ops)
|
106
|
+
else:
|
107
|
+
parallel_op.ops.append(op)
|
108
|
+
|
109
|
+
return parallel_op
|
110
|
+
|
111
|
+
|
112
|
+
def run1():
|
113
|
+
"""Basic test"""
|
114
|
+
|
115
|
+
class MockOp(BaseOp):
|
116
|
+
def execute(self):
|
117
|
+
logger.info(f"op={self.name} execute")
|
118
|
+
|
119
|
+
mock_op = MockOp()
|
120
|
+
mock_op()
|
121
|
+
|
122
|
+
|
123
|
+
def run2():
|
124
|
+
"""Test operator overloading functionality"""
|
125
|
+
from concurrent.futures import ThreadPoolExecutor
|
126
|
+
import time
|
127
|
+
|
128
|
+
class TestOp(BaseOp):
|
129
|
+
|
130
|
+
def execute(self, data=None):
|
131
|
+
time.sleep(0.1) # Simulate execution time
|
132
|
+
op_result = f"{self.name}({data})" if data else self.name
|
133
|
+
logger.info(f"Executing {op_result}")
|
134
|
+
return op_result
|
135
|
+
|
136
|
+
# Create service_context for parallel execution
|
137
|
+
C["thread_pool"] = ThreadPoolExecutor(max_workers=4)
|
138
|
+
|
139
|
+
# Create test operations
|
140
|
+
op1 = TestOp("op1")
|
141
|
+
op2 = TestOp("op2")
|
142
|
+
op3 = TestOp("op3")
|
143
|
+
op4 = TestOp("op4")
|
144
|
+
|
145
|
+
logger.info("=== Testing sequential execution op1 >> op2 ===")
|
146
|
+
sequential = op1 >> op2
|
147
|
+
result = sequential()
|
148
|
+
logger.info(f"Sequential result: {result}")
|
149
|
+
|
150
|
+
logger.info("=== Testing parallel execution op1 | op2 ===")
|
151
|
+
parallel = op1 | op2
|
152
|
+
result = parallel()
|
153
|
+
logger.info(f"Parallel result: {result}")
|
154
|
+
|
155
|
+
logger.info("=== Testing mixed calls op1 >> (op2 | op3) >> op4 ===")
|
156
|
+
mixed = op1 >> (op2 | op3) >> op4
|
157
|
+
result = mixed()
|
158
|
+
logger.info(f"Mixed result: {result}")
|
159
|
+
|
160
|
+
logger.info("=== Testing complex mixed calls op1 >> (op1 | (op2 >> op3)) >> op4 ===")
|
161
|
+
complex_mixed = op1 >> (op1 | (op2 >> op3)) >> op4
|
162
|
+
result = complex_mixed()
|
163
|
+
logger.info(f"Complex mixed result: {result}")
|
164
|
+
|
165
|
+
|
166
|
+
if __name__ == "__main__":
|
167
|
+
run1()
|
168
|
+
print("\n" + "=" * 50 + "\n")
|
169
|
+
run2()
|
@@ -0,0 +1,63 @@
|
|
1
|
+
from abc import ABC
|
2
|
+
from pathlib import Path
|
3
|
+
|
4
|
+
from flowllm.context.prompt_handler import PromptHandler
|
5
|
+
from flowllm.context.service_context import C
|
6
|
+
from flowllm.embedding_model.base_embedding_model import BaseEmbeddingModel
|
7
|
+
from flowllm.llm.base_llm import BaseLLM
|
8
|
+
from flowllm.op.base_op import BaseOp
|
9
|
+
from flowllm.schema.service_config import LLMConfig, EmbeddingModelConfig
|
10
|
+
from flowllm.storage.vector_store.base_vector_store import BaseVectorStore
|
11
|
+
|
12
|
+
|
13
|
+
class BaseLLMOp(BaseOp, ABC):
|
14
|
+
file_path: str = __file__
|
15
|
+
|
16
|
+
def __init__(self,
|
17
|
+
prompt_path: str = "",
|
18
|
+
llm: str = "default",
|
19
|
+
embedding_model: str = "default",
|
20
|
+
vector_store: str = "default",
|
21
|
+
**kwargs):
|
22
|
+
|
23
|
+
super().__init__(**kwargs)
|
24
|
+
|
25
|
+
self._llm: BaseLLM | str = llm
|
26
|
+
self._embedding_model: BaseEmbeddingModel | str = embedding_model
|
27
|
+
self._vector_store: BaseVectorStore | str = vector_store
|
28
|
+
|
29
|
+
default_prompt_path: Path = Path(self.file_path).parent / self.name.replace("_op", "_prompt.yaml")
|
30
|
+
self.prompt_path: Path = Path(prompt_path) if prompt_path else default_prompt_path
|
31
|
+
self.prompt = PromptHandler(language=self.language).load_prompt_by_file(self.prompt_path)
|
32
|
+
|
33
|
+
@property
|
34
|
+
def llm(self) -> BaseLLM:
|
35
|
+
if isinstance(self._llm, str):
|
36
|
+
llm_config: LLMConfig = self.flow_context.service_config.llm[self._llm]
|
37
|
+
llm_cls = C.resolve_llm(llm_config.backend)
|
38
|
+
self._llm = llm_cls(model_name=llm_config.model_name, **llm_config.params)
|
39
|
+
|
40
|
+
return self._llm
|
41
|
+
|
42
|
+
@property
|
43
|
+
def embedding_model(self) -> BaseEmbeddingModel:
|
44
|
+
if isinstance(self._embedding_model, str):
|
45
|
+
embedding_model_config: EmbeddingModelConfig = \
|
46
|
+
self.flow_context.service_config.embedding_model[self._embedding_model]
|
47
|
+
embedding_model_cls = C.resolve_embedding_model(embedding_model_config.backend)
|
48
|
+
self._embedding_model = embedding_model_cls(model_name=embedding_model_config.model_name,
|
49
|
+
**embedding_model_config.params)
|
50
|
+
|
51
|
+
return self._embedding_model
|
52
|
+
|
53
|
+
@property
|
54
|
+
def vector_store(self) -> BaseVectorStore:
|
55
|
+
if isinstance(self._vector_store, str):
|
56
|
+
self._vector_store = C.get_vector_store(self._vector_store)
|
57
|
+
return self._vector_store
|
58
|
+
|
59
|
+
def prompt_format(self, prompt_name: str, **kwargs) -> str:
|
60
|
+
return self.prompt.prompt_format(prompt_name=prompt_name, **kwargs)
|
61
|
+
|
62
|
+
def get_prompt(self, prompt_name: str) -> str:
|
63
|
+
return self.prompt.get_prompt(prompt_name=prompt_name)
|
flowllm/op/mock_op.py
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
import time
|
2
|
+
|
3
|
+
from loguru import logger
|
4
|
+
|
5
|
+
from flowllm.context.service_context import C
|
6
|
+
from flowllm.op.llm_base_op import BaseLLMOp
|
7
|
+
|
8
|
+
|
9
|
+
@C.register_op()
|
10
|
+
class Mock1Op(BaseLLMOp):
|
11
|
+
def execute(self):
|
12
|
+
time.sleep(1)
|
13
|
+
a = self.flow_context.a
|
14
|
+
b = self.flow_context.b
|
15
|
+
logger.info(f"enter class={self.name}. a={a} b={b}")
|
16
|
+
|
17
|
+
self.flow_context.response.answer = f"{self.name} {a} {b} answer=47"
|
18
|
+
|
19
|
+
|
20
|
+
@C.register_op()
|
21
|
+
class Mock2Op(Mock1Op):
|
22
|
+
...
|
23
|
+
|
24
|
+
|
25
|
+
@C.register_op()
|
26
|
+
class Mock3Op(Mock1Op):
|
27
|
+
...
|
28
|
+
|
29
|
+
|
30
|
+
@C.register_op()
|
31
|
+
class Mock4Op(Mock1Op):
|
32
|
+
...
|
33
|
+
|
34
|
+
|
35
|
+
@C.register_op()
|
36
|
+
class Mock5Op(Mock1Op):
|
37
|
+
...
|
38
|
+
|
39
|
+
|
40
|
+
@C.register_op()
|
41
|
+
class Mock6Op(Mock1Op):
|
42
|
+
...
|
@@ -0,0 +1,30 @@
|
|
1
|
+
from typing import List
|
2
|
+
|
3
|
+
from flowllm.op.base_op import BaseOp
|
4
|
+
|
5
|
+
|
6
|
+
class ParallelOp(BaseOp):
|
7
|
+
"""Container class for parallel operation execution
|
8
|
+
|
9
|
+
Executes multiple operations in parallel, all operations use the same input,
|
10
|
+
returns a list of results from all operations.
|
11
|
+
Supports parallel calls: op1 | op2 | op3
|
12
|
+
Falls back to sequential execution if no thread pool is available.
|
13
|
+
"""
|
14
|
+
|
15
|
+
def __init__(self, ops: List[BaseOp], **kwargs):
|
16
|
+
super().__init__(**kwargs)
|
17
|
+
self.ops = ops
|
18
|
+
|
19
|
+
def execute(self):
|
20
|
+
for op in self.ops:
|
21
|
+
self.submit_task(op.__call__)
|
22
|
+
|
23
|
+
return self.join_task(task_desc="Parallel execution")
|
24
|
+
|
25
|
+
def __or__(self, op: BaseOp):
|
26
|
+
if isinstance(op, ParallelOp):
|
27
|
+
self.ops.extend(op.ops)
|
28
|
+
else:
|
29
|
+
self.ops.append(op)
|
30
|
+
return self
|
@@ -0,0 +1,29 @@
|
|
1
|
+
from typing import List
|
2
|
+
|
3
|
+
from flowllm.op.base_op import BaseOp
|
4
|
+
|
5
|
+
|
6
|
+
class SequentialOp(BaseOp):
|
7
|
+
"""Container class for sequential operation execution
|
8
|
+
|
9
|
+
Executes multiple operations in sequence, where the output of the previous operation
|
10
|
+
becomes the input of the next operation.
|
11
|
+
Supports chaining: op1 >> op2 >> op3
|
12
|
+
"""
|
13
|
+
|
14
|
+
def __init__(self, ops: List[BaseOp], **kwargs):
|
15
|
+
super().__init__(**kwargs)
|
16
|
+
self.ops = ops
|
17
|
+
|
18
|
+
def execute(self):
|
19
|
+
result = None
|
20
|
+
for op in self.ops:
|
21
|
+
result = op.execute()
|
22
|
+
return result
|
23
|
+
|
24
|
+
def __rshift__(self, op: BaseOp):
|
25
|
+
if isinstance(op, SequentialOp):
|
26
|
+
self.ops.extend(op.ops)
|
27
|
+
else:
|
28
|
+
self.ops.append(op)
|
29
|
+
return self
|
@@ -0,0 +1,12 @@
|
|
1
|
+
from typing import List
|
2
|
+
|
3
|
+
from pydantic import Field, BaseModel
|
4
|
+
|
5
|
+
from flowllm.schema.message import Message
|
6
|
+
|
7
|
+
|
8
|
+
class FlowResponse(BaseModel):
|
9
|
+
answer: str = Field(default="")
|
10
|
+
messages: List[Message] = Field(default_factory=list)
|
11
|
+
success: bool = Field(default=True)
|
12
|
+
metadata: dict = Field(default_factory=dict)
|
@@ -0,0 +1,35 @@
|
|
1
|
+
from typing import List
|
2
|
+
|
3
|
+
from pydantic import BaseModel, Field
|
4
|
+
|
5
|
+
from flowllm.enumeration.role import Role
|
6
|
+
from flowllm.schema.tool_call import ToolCall
|
7
|
+
|
8
|
+
|
9
|
+
class Message(BaseModel):
|
10
|
+
role: Role = Field(default=Role.USER)
|
11
|
+
content: str | bytes = Field(default="")
|
12
|
+
reasoning_content: str = Field(default="")
|
13
|
+
tool_calls: List[ToolCall] = Field(default_factory=list)
|
14
|
+
tool_call_id: str = Field(default="")
|
15
|
+
metadata: dict = Field(default_factory=dict)
|
16
|
+
|
17
|
+
def simple_dump(self, add_reason_content: bool = True) -> dict:
|
18
|
+
result: dict
|
19
|
+
if self.content:
|
20
|
+
result = {"role": self.role.value, "content": self.content}
|
21
|
+
elif add_reason_content and self.reasoning_content:
|
22
|
+
result = {"role": self.role.value, "content": self.reasoning_content}
|
23
|
+
else:
|
24
|
+
result = {"role": self.role.value, "content": ""}
|
25
|
+
|
26
|
+
if self.tool_calls:
|
27
|
+
result["tool_calls"] = [x.simple_output_dump() for x in self.tool_calls]
|
28
|
+
return result
|
29
|
+
|
30
|
+
|
31
|
+
class Trajectory(BaseModel):
|
32
|
+
task_id: str = Field(default="")
|
33
|
+
messages: List[Message] = Field(default_factory=list)
|
34
|
+
score: float = Field(default=0.0)
|
35
|
+
metadata: dict = Field(default_factory=dict)
|