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.
Files changed (105) hide show
  1. flowllm/__init__.py +12 -0
  2. flowllm/app.py +25 -0
  3. flowllm/config/default_config.yaml +82 -0
  4. flowllm/config/pydantic_config_parser.py +242 -0
  5. flowllm/context/base_context.py +59 -0
  6. flowllm/context/flow_context.py +28 -0
  7. llmflow/op/prompt_mixin.py → flowllm/context/prompt_handler.py +25 -14
  8. flowllm/context/registry.py +26 -0
  9. flowllm/context/service_context.py +103 -0
  10. flowllm/embedding_model/__init__.py +1 -0
  11. {llmflow → flowllm}/embedding_model/base_embedding_model.py +2 -2
  12. {llmflow → flowllm}/embedding_model/openai_compatible_embedding_model.py +8 -8
  13. flowllm/flow_engine/__init__.py +1 -0
  14. flowllm/flow_engine/base_flow_engine.py +34 -0
  15. flowllm/flow_engine/simple_flow_engine.py +213 -0
  16. flowllm/llm/__init__.py +1 -0
  17. {llmflow → flowllm}/llm/base_llm.py +16 -24
  18. {llmflow → flowllm}/llm/openai_compatible_llm.py +64 -108
  19. flowllm/op/__init__.py +3 -0
  20. flowllm/op/akshare/get_ak_a_code_op.py +116 -0
  21. flowllm/op/akshare/get_ak_a_code_prompt.yaml +21 -0
  22. flowllm/op/akshare/get_ak_a_info_op.py +143 -0
  23. flowllm/op/base_op.py +169 -0
  24. flowllm/op/llm_base_op.py +63 -0
  25. flowllm/op/mock_op.py +42 -0
  26. flowllm/op/parallel_op.py +30 -0
  27. flowllm/op/sequential_op.py +29 -0
  28. flowllm/schema/flow_response.py +12 -0
  29. flowllm/schema/message.py +35 -0
  30. flowllm/schema/service_config.py +76 -0
  31. flowllm/schema/tool_call.py +110 -0
  32. flowllm/service/__init__.py +2 -0
  33. flowllm/service/base_service.py +59 -0
  34. flowllm/service/http_service.py +87 -0
  35. flowllm/service/mcp_service.py +45 -0
  36. flowllm/storage/__init__.py +1 -0
  37. flowllm/storage/vector_store/__init__.py +3 -0
  38. flowllm/storage/vector_store/base_vector_store.py +44 -0
  39. {llmflow → flowllm/storage}/vector_store/chroma_vector_store.py +11 -10
  40. {llmflow → flowllm/storage}/vector_store/es_vector_store.py +10 -9
  41. llmflow/vector_store/file_vector_store.py → flowllm/storage/vector_store/local_vector_store.py +110 -10
  42. flowllm/utils/common_utils.py +64 -0
  43. flowllm/utils/dataframe_cache.py +331 -0
  44. flowllm/utils/fetch_url.py +113 -0
  45. {llmflow → flowllm}/utils/timer.py +5 -4
  46. {flowllm-0.1.0.dist-info → flowllm-0.1.1.dist-info}/METADATA +31 -27
  47. flowllm-0.1.1.dist-info/RECORD +62 -0
  48. flowllm-0.1.1.dist-info/entry_points.txt +4 -0
  49. {flowllm-0.1.0.dist-info → flowllm-0.1.1.dist-info}/licenses/LICENSE +1 -1
  50. flowllm-0.1.1.dist-info/top_level.txt +1 -0
  51. flowllm-0.1.0.dist-info/RECORD +0 -66
  52. flowllm-0.1.0.dist-info/entry_points.txt +0 -3
  53. flowllm-0.1.0.dist-info/top_level.txt +0 -1
  54. llmflow/app.py +0 -53
  55. llmflow/config/config_parser.py +0 -80
  56. llmflow/config/mock_config.yaml +0 -58
  57. llmflow/embedding_model/__init__.py +0 -5
  58. llmflow/enumeration/agent_state.py +0 -8
  59. llmflow/llm/__init__.py +0 -5
  60. llmflow/mcp_server.py +0 -110
  61. llmflow/op/__init__.py +0 -10
  62. llmflow/op/base_op.py +0 -125
  63. llmflow/op/mock_op.py +0 -40
  64. llmflow/op/react/react_v1_op.py +0 -88
  65. llmflow/op/react/react_v1_prompt.yaml +0 -28
  66. llmflow/op/vector_store/__init__.py +0 -13
  67. llmflow/op/vector_store/recall_vector_store_op.py +0 -48
  68. llmflow/op/vector_store/update_vector_store_op.py +0 -28
  69. llmflow/op/vector_store/vector_store_action_op.py +0 -46
  70. llmflow/pipeline/pipeline.py +0 -94
  71. llmflow/pipeline/pipeline_context.py +0 -37
  72. llmflow/schema/app_config.py +0 -69
  73. llmflow/schema/experience.py +0 -144
  74. llmflow/schema/message.py +0 -68
  75. llmflow/schema/request.py +0 -32
  76. llmflow/schema/response.py +0 -29
  77. llmflow/service/__init__.py +0 -0
  78. llmflow/service/llmflow_service.py +0 -96
  79. llmflow/tool/__init__.py +0 -9
  80. llmflow/tool/base_tool.py +0 -80
  81. llmflow/tool/code_tool.py +0 -43
  82. llmflow/tool/dashscope_search_tool.py +0 -162
  83. llmflow/tool/mcp_tool.py +0 -77
  84. llmflow/tool/tavily_search_tool.py +0 -109
  85. llmflow/tool/terminate_tool.py +0 -23
  86. llmflow/utils/__init__.py +0 -0
  87. llmflow/utils/common_utils.py +0 -17
  88. llmflow/utils/file_handler.py +0 -25
  89. llmflow/utils/http_client.py +0 -156
  90. llmflow/utils/op_utils.py +0 -102
  91. llmflow/utils/registry.py +0 -33
  92. llmflow/vector_store/__init__.py +0 -7
  93. llmflow/vector_store/base_vector_store.py +0 -136
  94. {llmflow → flowllm/config}/__init__.py +0 -0
  95. {llmflow/config → flowllm/context}/__init__.py +0 -0
  96. {llmflow → flowllm}/enumeration/__init__.py +0 -0
  97. {llmflow → flowllm}/enumeration/chunk_enum.py +0 -0
  98. {llmflow → flowllm}/enumeration/http_enum.py +0 -0
  99. {llmflow → flowllm}/enumeration/role.py +0 -0
  100. {llmflow/op/react → flowllm/op/akshare}/__init__.py +0 -0
  101. {llmflow/pipeline → flowllm/schema}/__init__.py +0 -0
  102. {llmflow → flowllm}/schema/vector_node.py +0 -0
  103. {llmflow/schema → flowllm/utils}/__init__.py +0 -0
  104. {llmflow → flowllm}/utils/singleton.py +0 -0
  105. {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)