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.
Files changed (66) hide show
  1. flowllm-0.1.0.dist-info/METADATA +597 -0
  2. flowllm-0.1.0.dist-info/RECORD +66 -0
  3. flowllm-0.1.0.dist-info/WHEEL +5 -0
  4. flowllm-0.1.0.dist-info/entry_points.txt +3 -0
  5. flowllm-0.1.0.dist-info/licenses/LICENSE +201 -0
  6. flowllm-0.1.0.dist-info/top_level.txt +1 -0
  7. llmflow/__init__.py +0 -0
  8. llmflow/app.py +53 -0
  9. llmflow/config/__init__.py +0 -0
  10. llmflow/config/config_parser.py +80 -0
  11. llmflow/config/mock_config.yaml +58 -0
  12. llmflow/embedding_model/__init__.py +5 -0
  13. llmflow/embedding_model/base_embedding_model.py +104 -0
  14. llmflow/embedding_model/openai_compatible_embedding_model.py +95 -0
  15. llmflow/enumeration/__init__.py +0 -0
  16. llmflow/enumeration/agent_state.py +8 -0
  17. llmflow/enumeration/chunk_enum.py +9 -0
  18. llmflow/enumeration/http_enum.py +9 -0
  19. llmflow/enumeration/role.py +8 -0
  20. llmflow/llm/__init__.py +5 -0
  21. llmflow/llm/base_llm.py +138 -0
  22. llmflow/llm/openai_compatible_llm.py +283 -0
  23. llmflow/mcp_server.py +110 -0
  24. llmflow/op/__init__.py +10 -0
  25. llmflow/op/base_op.py +125 -0
  26. llmflow/op/mock_op.py +40 -0
  27. llmflow/op/prompt_mixin.py +74 -0
  28. llmflow/op/react/__init__.py +0 -0
  29. llmflow/op/react/react_v1_op.py +88 -0
  30. llmflow/op/react/react_v1_prompt.yaml +28 -0
  31. llmflow/op/vector_store/__init__.py +13 -0
  32. llmflow/op/vector_store/recall_vector_store_op.py +48 -0
  33. llmflow/op/vector_store/update_vector_store_op.py +28 -0
  34. llmflow/op/vector_store/vector_store_action_op.py +46 -0
  35. llmflow/pipeline/__init__.py +0 -0
  36. llmflow/pipeline/pipeline.py +94 -0
  37. llmflow/pipeline/pipeline_context.py +37 -0
  38. llmflow/schema/__init__.py +0 -0
  39. llmflow/schema/app_config.py +69 -0
  40. llmflow/schema/experience.py +144 -0
  41. llmflow/schema/message.py +68 -0
  42. llmflow/schema/request.py +32 -0
  43. llmflow/schema/response.py +29 -0
  44. llmflow/schema/vector_node.py +11 -0
  45. llmflow/service/__init__.py +0 -0
  46. llmflow/service/llmflow_service.py +96 -0
  47. llmflow/tool/__init__.py +9 -0
  48. llmflow/tool/base_tool.py +80 -0
  49. llmflow/tool/code_tool.py +43 -0
  50. llmflow/tool/dashscope_search_tool.py +162 -0
  51. llmflow/tool/mcp_tool.py +77 -0
  52. llmflow/tool/tavily_search_tool.py +109 -0
  53. llmflow/tool/terminate_tool.py +23 -0
  54. llmflow/utils/__init__.py +0 -0
  55. llmflow/utils/common_utils.py +17 -0
  56. llmflow/utils/file_handler.py +25 -0
  57. llmflow/utils/http_client.py +156 -0
  58. llmflow/utils/op_utils.py +102 -0
  59. llmflow/utils/registry.py +33 -0
  60. llmflow/utils/singleton.py +9 -0
  61. llmflow/utils/timer.py +53 -0
  62. llmflow/vector_store/__init__.py +7 -0
  63. llmflow/vector_store/base_vector_store.py +136 -0
  64. llmflow/vector_store/chroma_vector_store.py +188 -0
  65. llmflow/vector_store/es_vector_store.py +227 -0
  66. llmflow/vector_store/file_vector_store.py +163 -0
@@ -0,0 +1,138 @@
1
+ import time
2
+ from abc import ABC
3
+ from typing import List, Literal, Callable
4
+
5
+ from loguru import logger
6
+ from pydantic import Field, BaseModel
7
+
8
+ from llmflow.schema.message import Message
9
+ from llmflow.tool.base_tool import BaseTool
10
+
11
+
12
+ class BaseLLM(BaseModel, ABC):
13
+ """
14
+ Abstract base class for Large Language Model (LLM) implementations.
15
+
16
+ This class defines the common interface and configuration parameters
17
+ that all LLM implementations should support. It provides a standardized
18
+ way to interact with different LLM providers while handling common
19
+ concerns like retries, error handling, and streaming.
20
+ """
21
+ # Core model configuration
22
+ model_name: str = Field(..., description="Name of the LLM model to use")
23
+
24
+ # Generation parameters
25
+ seed: int = Field(default=42, description="Random seed for reproducible outputs")
26
+ top_p: float | None = Field(default=None, description="Top-p (nucleus) sampling parameter")
27
+ # stream: bool = Field(default=True) # Commented out - streaming is handled per request
28
+ stream_options: dict = Field(default={"include_usage": True}, description="Options for streaming responses")
29
+ temperature: float = Field(default=0.0000001, description="Sampling temperature (low for deterministic outputs)")
30
+ presence_penalty: float | None = Field(default=None, description="Presence penalty to reduce repetition")
31
+
32
+ # Model-specific features
33
+ enable_thinking: bool = Field(default=True, description="Enable reasoning/thinking mode for supported models")
34
+
35
+ # Tool usage configuration
36
+ tool_choice: Literal["none", "auto", "required"] = Field(default="auto", description="Strategy for tool selection")
37
+ parallel_tool_calls: bool = Field(default=True, description="Allow multiple tool calls in parallel")
38
+
39
+ # Error handling and reliability
40
+ max_retries: int = Field(default=5, description="Maximum number of retry attempts on failure")
41
+ raise_exception: bool = Field(default=False, description="Whether to raise exceptions or return default values")
42
+
43
+ def stream_chat(self, messages: List[Message], tools: List[BaseTool] = None, **kwargs):
44
+ """
45
+ Stream chat completions from the LLM.
46
+
47
+ This method should yield chunks of the response as they become available,
48
+ allowing for real-time display of the model's output.
49
+
50
+ Args:
51
+ messages: List of conversation messages
52
+ tools: Optional list of tools the model can use
53
+ **kwargs: Additional model-specific parameters
54
+
55
+ Yields:
56
+ Chunks of the streaming response with their types
57
+ """
58
+ raise NotImplementedError
59
+
60
+ def stream_print(self, messages: List[Message], tools: List[BaseTool] = None, **kwargs):
61
+ """
62
+ Stream chat completions and print them to console in real-time.
63
+
64
+ This is a convenience method for debugging and interactive use,
65
+ combining streaming with formatted console output.
66
+
67
+ Args:
68
+ messages: List of conversation messages
69
+ tools: Optional list of tools the model can use
70
+ **kwargs: Additional model-specific parameters
71
+ """
72
+ raise NotImplementedError
73
+
74
+ def _chat(self, messages: List[Message], tools: List[BaseTool] = None, **kwargs) -> Message:
75
+ """
76
+ Internal method to perform a single chat completion.
77
+
78
+ This method should be implemented by subclasses to handle the actual
79
+ communication with the LLM provider. It's called by the public chat()
80
+ method which adds retry logic and error handling.
81
+
82
+ Args:
83
+ messages: List of conversation messages
84
+ tools: Optional list of tools the model can use
85
+ **kwargs: Additional model-specific parameters
86
+
87
+ Returns:
88
+ The complete response message from the LLM
89
+ """
90
+ raise NotImplementedError
91
+
92
+ def chat(self, messages: List[Message], tools: List[BaseTool] = None, callback_fn: Callable = None,
93
+ default_value=None, **kwargs):
94
+ """
95
+ Perform a chat completion with retry logic and error handling.
96
+
97
+ This is the main public interface for chat completions. It wraps the
98
+ internal _chat() method with robust error handling, exponential backoff,
99
+ and optional callback processing.
100
+
101
+ Args:
102
+ messages: List of conversation messages
103
+ tools: Optional list of tools the model can use
104
+ callback_fn: Optional callback to process the response message
105
+ default_value: Value to return if all retries fail (when raise_exception=False)
106
+ **kwargs: Additional model-specific parameters
107
+
108
+ Returns:
109
+ The response message (possibly processed by callback_fn) or default_value
110
+
111
+ Raises:
112
+ Exception: If raise_exception=True and all retries fail
113
+ """
114
+ for i in range(self.max_retries):
115
+ try:
116
+ # Attempt to get response from the model
117
+ message: Message = self._chat(messages, tools, **kwargs)
118
+
119
+ # Apply callback function if provided
120
+ if callback_fn:
121
+ return callback_fn(message)
122
+ else:
123
+ return message
124
+
125
+ except Exception as e:
126
+ logger.exception(f"chat with model={self.model_name} encounter error with e={e.args}")
127
+
128
+ # Exponential backoff: wait longer after each failure
129
+ time.sleep(1 + i)
130
+
131
+ # Handle final retry failure
132
+ if i == self.max_retries - 1:
133
+ if self.raise_exception:
134
+ raise e
135
+ else:
136
+ return default_value
137
+
138
+ return None
@@ -0,0 +1,283 @@
1
+ import os
2
+ from typing import List
3
+
4
+ from dotenv import load_dotenv
5
+ from loguru import logger
6
+ from openai import OpenAI
7
+ from openai.types import CompletionUsage
8
+ from pydantic import Field, PrivateAttr, model_validator
9
+
10
+ from llmflow.enumeration.chunk_enum import ChunkEnum
11
+ from llmflow.enumeration.role import Role
12
+ from llmflow.llm import LLM_REGISTRY
13
+ from llmflow.llm.base_llm import BaseLLM
14
+ from llmflow.schema.message import Message, ToolCall
15
+ from llmflow.tool.base_tool import BaseTool
16
+
17
+
18
+ @LLM_REGISTRY.register("openai_compatible")
19
+ class OpenAICompatibleBaseLLM(BaseLLM):
20
+ """
21
+ OpenAI-compatible LLM implementation supporting streaming and tool calls.
22
+
23
+ This class implements the BaseLLM interface for OpenAI-compatible APIs,
24
+ including support for:
25
+ - Streaming responses with different chunk types (thinking, answer, tools)
26
+ - Tool calling with parallel execution
27
+ - Reasoning/thinking content from supported models
28
+ - Robust error handling and retries
29
+ """
30
+
31
+ # API configuration
32
+ api_key: str = Field(default_factory=lambda: os.getenv("LLM_API_KEY"), description="API key for authentication")
33
+ base_url: str = Field(default_factory=lambda: os.getenv("LLM_BASE_URL"),
34
+ description="Base URL for the API endpoint")
35
+ _client: OpenAI = PrivateAttr()
36
+
37
+ @model_validator(mode="after")
38
+ def init_client(self):
39
+ """
40
+ Initialize the OpenAI client after model validation.
41
+
42
+ This validator runs after all field validation is complete,
43
+ ensuring we have valid API credentials before creating the client.
44
+
45
+ Returns:
46
+ Self for method chaining
47
+ """
48
+ self._client = OpenAI(api_key=self.api_key, base_url=self.base_url)
49
+ return self
50
+
51
+ def stream_chat(self, messages: List[Message], tools: List[BaseTool] = None, **kwargs):
52
+ """
53
+ Stream chat completions from OpenAI-compatible API.
54
+
55
+ This method handles streaming responses and categorizes chunks into different types:
56
+ - THINK: Reasoning/thinking content from the model
57
+ - ANSWER: Regular response content
58
+ - TOOL: Tool calls that need to be executed
59
+ - USAGE: Token usage statistics
60
+ - ERROR: Error information
61
+
62
+ Args:
63
+ messages: List of conversation messages
64
+ tools: Optional list of tools available to the model
65
+ **kwargs: Additional parameters
66
+
67
+ Yields:
68
+ Tuple of (chunk_content, ChunkEnum) for each streaming piece
69
+ """
70
+ for i in range(self.max_retries):
71
+ try:
72
+ # Create streaming completion request
73
+ completion = self._client.chat.completions.create(
74
+ model=self.model_name,
75
+ messages=[x.simple_dump() for x in messages],
76
+ seed=self.seed,
77
+ top_p=self.top_p,
78
+ stream=True,
79
+ stream_options=self.stream_options,
80
+ temperature=self.temperature,
81
+ extra_body={"enable_thinking": self.enable_thinking}, # Enable reasoning mode
82
+ tools=[x.simple_dump() for x in tools] if tools else None,
83
+ tool_choice=self.tool_choice,
84
+ parallel_tool_calls=self.parallel_tool_calls)
85
+
86
+ # Initialize tool call tracking
87
+ ret_tools = [] # Accumulate tool calls across chunks
88
+ is_answering = False # Track when model starts answering
89
+
90
+ # Process each chunk in the streaming response
91
+ for chunk in completion:
92
+ # Handle chunks without choices (usually usage info)
93
+ if not chunk.choices:
94
+ yield chunk.usage, ChunkEnum.USAGE
95
+
96
+ else:
97
+ delta = chunk.choices[0].delta
98
+
99
+ # Handle reasoning/thinking content (model's internal thoughts)
100
+ if hasattr(delta, 'reasoning_content') and delta.reasoning_content is not None:
101
+ yield delta.reasoning_content, ChunkEnum.THINK
102
+
103
+ else:
104
+ # Mark transition from thinking to answering
105
+ if not is_answering:
106
+ is_answering = True
107
+
108
+ # Handle regular response content
109
+ if delta.content is not None:
110
+ yield delta.content, ChunkEnum.ANSWER
111
+
112
+ # Handle tool calls (function calling)
113
+ if delta.tool_calls is not None:
114
+ for tool_call in delta.tool_calls:
115
+ index = tool_call.index
116
+
117
+ # Ensure we have enough tool call slots
118
+ while len(ret_tools) <= index:
119
+ ret_tools.append(ToolCall(index=index))
120
+
121
+ # Accumulate tool call information across chunks
122
+ if tool_call.id:
123
+ ret_tools[index].id += tool_call.id
124
+
125
+ if tool_call.function and tool_call.function.name:
126
+ ret_tools[index].name += tool_call.function.name
127
+
128
+ if tool_call.function and tool_call.function.arguments:
129
+ ret_tools[index].arguments += tool_call.function.arguments
130
+
131
+ # Yield completed tool calls after streaming finishes
132
+ if ret_tools:
133
+ tool_dict = {x.name: x for x in tools} if tools else {}
134
+ for tool in ret_tools:
135
+ # Only yield tool calls that correspond to available tools
136
+ if tool.name not in tool_dict:
137
+ continue
138
+
139
+ yield tool, ChunkEnum.TOOL
140
+
141
+ return # Success - exit retry loop
142
+
143
+ except Exception as e:
144
+ logger.exception(f"stream chat with model={self.model_name} encounter error with e={e.args}")
145
+
146
+ # Handle retry logic
147
+ if i == self.max_retries - 1 and self.raise_exception:
148
+ raise e
149
+ else:
150
+ yield e.args, ChunkEnum.ERROR
151
+
152
+ def _chat(self, messages: List[Message], tools: List[BaseTool] = None, **kwargs) -> Message:
153
+ """
154
+ Perform a complete chat completion by aggregating streaming chunks.
155
+
156
+ This method consumes the entire streaming response and combines all
157
+ chunks into a single Message object. It separates reasoning content,
158
+ regular answer content, and tool calls.
159
+
160
+ Args:
161
+ messages: List of conversation messages
162
+ tools: Optional list of tools available to the model
163
+ **kwargs: Additional parameters
164
+
165
+ Returns:
166
+ Complete Message with all content aggregated
167
+ """
168
+ # Initialize content accumulators
169
+ reasoning_content = "" # Model's internal reasoning
170
+ answer_content = "" # Final response content
171
+ tool_calls = [] # List of tool calls to execute
172
+
173
+ # Consume streaming response and aggregate chunks by type
174
+ for chunk, chunk_enum in self.stream_chat(messages, tools, **kwargs):
175
+ if chunk_enum is ChunkEnum.THINK:
176
+ reasoning_content += chunk
177
+
178
+ elif chunk_enum is ChunkEnum.ANSWER:
179
+ answer_content += chunk
180
+
181
+ elif chunk_enum is ChunkEnum.TOOL:
182
+ tool_calls.append(chunk)
183
+
184
+ # Note: USAGE and ERROR chunks are ignored in non-streaming mode
185
+
186
+ # Construct complete response message
187
+ return Message(role=Role.ASSISTANT,
188
+ reasoning_content=reasoning_content,
189
+ content=answer_content,
190
+ tool_calls=tool_calls)
191
+
192
+ def stream_print(self, messages: List[Message], tools: List[BaseTool] = None, **kwargs):
193
+ """
194
+ Stream chat completions with formatted console output.
195
+
196
+ This method provides a real-time view of the model's response,
197
+ with different formatting for different types of content:
198
+ - Thinking content is wrapped in <think></think> tags
199
+ - Answer content is printed directly
200
+ - Tool calls are formatted as JSON
201
+ - Usage statistics and errors are clearly marked
202
+
203
+ Args:
204
+ messages: List of conversation messages
205
+ tools: Optional list of tools available to the model
206
+ **kwargs: Additional parameters
207
+ """
208
+ # Track which sections we've entered for proper formatting
209
+ enter_think = False # Whether we've started printing thinking content
210
+ enter_answer = False # Whether we've started printing answer content
211
+
212
+ # Process each streaming chunk with appropriate formatting
213
+ for chunk, chunk_enum in self.stream_chat(messages, tools, **kwargs):
214
+ if chunk_enum is ChunkEnum.USAGE:
215
+ # Display token usage statistics
216
+ if isinstance(chunk, CompletionUsage):
217
+ print(f"\n<usage>{chunk.model_dump_json(indent=2)}</usage>")
218
+ else:
219
+ print(f"\n<usage>{chunk}</usage>")
220
+
221
+ elif chunk_enum is ChunkEnum.THINK:
222
+ # Format thinking/reasoning content
223
+ if not enter_think:
224
+ enter_think = True
225
+ print("<think>\n", end="")
226
+ print(chunk, end="")
227
+
228
+ elif chunk_enum is ChunkEnum.ANSWER:
229
+ # Format regular answer content
230
+ if not enter_answer:
231
+ enter_answer = True
232
+ # Close thinking section if we were in it
233
+ if enter_think:
234
+ print("\n</think>")
235
+ print(chunk, end="")
236
+
237
+ elif chunk_enum is ChunkEnum.TOOL:
238
+ # Format tool calls as structured JSON
239
+ assert isinstance(chunk, ToolCall)
240
+ print(f"\n<tool>{chunk.model_dump_json(indent=2)}</tool>", end="")
241
+
242
+ elif chunk_enum is ChunkEnum.ERROR:
243
+ # Display error information
244
+ print(f"\n<error>{chunk}</error>", end="")
245
+
246
+
247
+ def main():
248
+ """
249
+ Demo function to test the OpenAI-compatible LLM implementation.
250
+
251
+ This function demonstrates:
252
+ 1. Basic chat without tools
253
+ 2. Chat with tool usage (search and code tools)
254
+ 3. Real-time streaming output formatting
255
+ """
256
+ from llmflow.tool.dashscope_search_tool import DashscopeSearchTool
257
+ from llmflow.tool.code_tool import CodeTool
258
+ from llmflow.enumeration.role import Role
259
+
260
+ # Load environment variables for API credentials
261
+ load_dotenv()
262
+
263
+ # Initialize the LLM with a specific model
264
+ model_name = "qwen-max-2025-01-25"
265
+ llm = OpenAICompatibleBaseLLM(model_name=model_name)
266
+
267
+ # Set up available tools
268
+ tools: List[BaseTool] = [DashscopeSearchTool(), CodeTool()]
269
+
270
+ # Test 1: Simple greeting without tools
271
+ print("=== Test 1: Simple Chat ===")
272
+ llm.stream_print([Message(role=Role.USER, content="hello")], [])
273
+
274
+ print("\n" + "=" * 20)
275
+
276
+ # Test 2: Complex query that might use tools
277
+ print("\n=== Test 2: Chat with Tools ===")
278
+ llm.stream_print([Message(role=Role.USER, content="What's the weather like in Beijing today?")], tools)
279
+
280
+
281
+ if __name__ == "__main__":
282
+ main()
283
+ # Launch with: python -m llmflow.llm.openai_compatible_llm
llmflow/mcp_server.py ADDED
@@ -0,0 +1,110 @@
1
+ import sys
2
+ from typing import List
3
+
4
+ from dotenv import load_dotenv
5
+ from fastmcp import FastMCP
6
+
7
+ from llmflow.service.llmflow_service import LLMFlowService
8
+
9
+ load_dotenv()
10
+
11
+ mcp = FastMCP("llmflow")
12
+ service = LLMFlowService(sys.argv[1:])
13
+
14
+
15
+ @mcp.tool
16
+ def retriever(query: str,
17
+ messages: List[dict] = None,
18
+ top_k: int = 1,
19
+ workspace_id: str = "default",
20
+ config: dict = None) -> dict:
21
+ """
22
+ Retrieve experiences from the workspace based on a query.
23
+
24
+ Args:
25
+ query: Query string
26
+ messages: List of messages
27
+ top_k: Number of top experiences to retrieve
28
+ workspace_id: Workspace identifier
29
+ config: Additional configuration parameters
30
+
31
+ Returns:
32
+ Dictionary containing retrieved experiences
33
+ """
34
+ return service(api="retriever", request={
35
+ "query": query,
36
+ "messages": messages if messages else [],
37
+ "top_k": top_k,
38
+ "workspace_id": workspace_id,
39
+ "config": config if config else {},
40
+ }).model_dump()
41
+
42
+
43
+ @mcp.tool
44
+ def summarizer(traj_list: List[dict], workspace_id: str = "default", config: dict = None) -> dict:
45
+ """
46
+ Summarize trajectories into experiences.
47
+
48
+ Args:
49
+ traj_list: List of trajectories
50
+ workspace_id: Workspace identifier
51
+ config: Additional configuration parameters
52
+
53
+ Returns:
54
+ experiences
55
+ """
56
+ return service(api="summarizer", request={
57
+ "traj_list": traj_list,
58
+ "workspace_id": workspace_id,
59
+ "config": config if config else {},
60
+ }).model_dump()
61
+
62
+
63
+ @mcp.tool
64
+ def vector_store(action: str,
65
+ src_workspace_id: str = "",
66
+ workspace_id: str = "",
67
+ path: str = "./",
68
+ config: dict = None) -> dict:
69
+ """
70
+ Perform vector store operations.
71
+
72
+ Args:
73
+ action: Action to perform (e.g., "copy", "delete", "dump", "load")
74
+ src_workspace_id: Source workspace identifier
75
+ workspace_id: Workspace identifier
76
+ path: Path to the vector store
77
+ config: Additional configuration parameters
78
+
79
+ Returns:
80
+ Dictionary containing the result of the vector store operation
81
+ """
82
+ return service(api="vector_store", request={
83
+ "action": action,
84
+ "src_workspace_id": src_workspace_id,
85
+ "workspace_id": workspace_id,
86
+ "path": path,
87
+ "config": config if config else {},
88
+ }).model_dump()
89
+
90
+
91
+ def main():
92
+ mcp_transport: str = service.init_app_config.mcp_transport
93
+ if mcp_transport == "sse":
94
+ mcp.run(transport="sse", host=service.http_service_config.host, port=service.http_service_config.port)
95
+ elif mcp_transport == "stdio":
96
+ mcp.run(transport="stdio")
97
+ else:
98
+ raise ValueError(f"Unsupported mcp transport: {mcp_transport}")
99
+
100
+
101
+ if __name__ == "__main__":
102
+ main()
103
+
104
+ # start with:
105
+ # llmflow_mcp \
106
+ # mcp_transport=stdio \
107
+ # http_service.port=8001 \
108
+ # llm.default.model_name=qwen3-32b \
109
+ # embedding_model.default.model_name=text-embedding-v4 \
110
+ # vector_store.default.backend=local_file
llmflow/op/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ from llmflow.utils.registry import Registry
2
+
3
+ OP_REGISTRY = Registry()
4
+
5
+ from llmflow.op.mock_op import Mock1Op, Mock2Op, Mock3Op, Mock4Op, Mock5Op, Mock6Op
6
+
7
+ from llmflow.op.vector_store.update_vector_store_op import UpdateVectorStoreOp
8
+ from llmflow.op.vector_store.recall_vector_store_op import RecallVectorStoreOp
9
+ from llmflow.op.vector_store.vector_store_action_op import VectorStoreActionOp
10
+ from llmflow.op.react.react_v1_op import ReactV1Op
llmflow/op/base_op.py ADDED
@@ -0,0 +1,125 @@
1
+ from abc import abstractmethod, ABC
2
+ from concurrent.futures import Future
3
+ from pathlib import Path
4
+ from typing import List
5
+
6
+ from loguru import logger
7
+ from tqdm import tqdm
8
+
9
+ from llmflow.embedding_model import EMBEDDING_MODEL_REGISTRY
10
+ from llmflow.embedding_model.base_embedding_model import BaseEmbeddingModel
11
+ from llmflow.llm import LLM_REGISTRY
12
+ from llmflow.llm.base_llm import BaseLLM
13
+ from llmflow.op.prompt_mixin import PromptMixin
14
+ from llmflow.pipeline.pipeline_context import PipelineContext
15
+ from llmflow.schema.app_config import OpConfig, LLMConfig, EmbeddingModelConfig
16
+ from llmflow.utils.common_utils import camel_to_snake
17
+ from llmflow.utils.timer import Timer
18
+ from llmflow.vector_store.base_vector_store import BaseVectorStore
19
+
20
+
21
+ class BaseOp(PromptMixin, ABC):
22
+ current_path: str = __file__
23
+
24
+ def __init__(self, context: PipelineContext, op_config: OpConfig):
25
+ super().__init__()
26
+ self.context: PipelineContext = context
27
+ self.op_config: OpConfig = op_config
28
+ self.timer = Timer(name=self.simple_name)
29
+
30
+ self._prepare_prompt()
31
+
32
+ self._llm: BaseLLM | None = None
33
+ self._embedding_model: BaseEmbeddingModel | None = None
34
+ self._vector_store: BaseVectorStore | None = None
35
+
36
+ self.task_list: List[Future] = []
37
+
38
+ def _prepare_prompt(self):
39
+ if self.op_config.prompt_file_path:
40
+ prompt_file_path = self.op_config.prompt_file_path
41
+ else:
42
+ prompt_name = self.simple_name.replace("_op", "_prompt.yaml")
43
+ prompt_file_path = Path(self.current_path).parent / prompt_name
44
+
45
+ # Load custom prompts from prompt file
46
+ self.load_prompt_by_file(prompt_file_path=prompt_file_path)
47
+
48
+ # Load custom prompts from config
49
+ self.load_prompt_dict(prompt_dict=self.op_config.prompt_dict)
50
+
51
+ @property
52
+ def simple_name(self) -> str:
53
+ return camel_to_snake(self.__class__.__name__)
54
+
55
+ @property
56
+ def op_params(self) -> dict:
57
+ return self.op_config.params
58
+
59
+ @abstractmethod
60
+ def execute(self):
61
+ ...
62
+
63
+ def execute_wrap(self):
64
+ try:
65
+ with self.timer:
66
+ return self.execute()
67
+
68
+ except Exception as e:
69
+ logger.exception(f"op={self.simple_name} execute failed, error={e.args}")
70
+
71
+ def submit_task(self, fn, *args, **kwargs):
72
+ task = self.context.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.simple_name + ".join_task")):
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
+ @property
89
+ def llm(self) -> BaseLLM:
90
+ if self._llm is None:
91
+ llm_name: str = self.op_config.llm
92
+ assert llm_name in self.context.app_config.llm, f"llm={llm_name} not found in app_config.llm!"
93
+ llm_config: LLMConfig = self.context.app_config.llm[llm_name]
94
+
95
+ assert llm_config.backend in LLM_REGISTRY, f"llm.backend={llm_config.backend} not found in LLM_REGISTRY!"
96
+ llm_cls = LLM_REGISTRY[llm_config.backend]
97
+ self._llm = llm_cls(model_name=llm_config.model_name, **llm_config.params)
98
+
99
+ return self._llm
100
+
101
+ @property
102
+ def embedding_model(self):
103
+ if self._embedding_model is None:
104
+ embedding_model_name: str = self.op_config.embedding_model
105
+ assert embedding_model_name in self.context.app_config.embedding_model, \
106
+ f"embedding_model={embedding_model_name} not found in app_config.embedding_model!"
107
+ embedding_model_config: EmbeddingModelConfig = self.context.app_config.embedding_model[embedding_model_name]
108
+
109
+ assert embedding_model_config.backend in EMBEDDING_MODEL_REGISTRY, \
110
+ f"embedding_model.backend={embedding_model_config.backend} not found in EMBEDDING_MODEL_REGISTRY!"
111
+ embedding_model_cls = EMBEDDING_MODEL_REGISTRY[embedding_model_config.backend]
112
+ self._embedding_model = embedding_model_cls(model_name=embedding_model_config.model_name,
113
+ **embedding_model_config.params)
114
+
115
+ return self._embedding_model
116
+
117
+ @property
118
+ def vector_store(self):
119
+ if self._vector_store is None:
120
+ vector_store_name: str = self.op_config.vector_store
121
+ assert vector_store_name in self.context.vector_store_dict, \
122
+ f"vector_store={vector_store_name} not found in vector_store_dict!"
123
+ self._vector_store = self.context.vector_store_dict[vector_store_name]
124
+
125
+ return self._vector_store