flowllm 0.1.0__py3-none-any.whl → 0.1.2__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 (141) hide show
  1. flowllm/__init__.py +21 -0
  2. flowllm/app.py +15 -0
  3. flowllm/client/__init__.py +25 -0
  4. flowllm/client/async_http_client.py +81 -0
  5. flowllm/client/http_client.py +81 -0
  6. flowllm/client/mcp_client.py +133 -0
  7. flowllm/client/sync_mcp_client.py +116 -0
  8. flowllm/config/__init__.py +1 -0
  9. flowllm/config/default.yaml +77 -0
  10. flowllm/config/empty.yaml +37 -0
  11. flowllm/config/pydantic_config_parser.py +242 -0
  12. flowllm/context/base_context.py +79 -0
  13. flowllm/context/flow_context.py +16 -0
  14. llmflow/op/prompt_mixin.py → flowllm/context/prompt_handler.py +25 -14
  15. flowllm/context/registry.py +30 -0
  16. flowllm/context/service_context.py +147 -0
  17. flowllm/embedding_model/__init__.py +1 -0
  18. {llmflow → flowllm}/embedding_model/base_embedding_model.py +93 -2
  19. {llmflow → flowllm}/embedding_model/openai_compatible_embedding_model.py +71 -13
  20. flowllm/flow/__init__.py +1 -0
  21. flowllm/flow/base_flow.py +72 -0
  22. flowllm/flow/base_tool_flow.py +15 -0
  23. flowllm/flow/gallery/__init__.py +8 -0
  24. flowllm/flow/gallery/cmd_flow.py +11 -0
  25. flowllm/flow/gallery/code_tool_flow.py +30 -0
  26. flowllm/flow/gallery/dashscope_search_tool_flow.py +34 -0
  27. flowllm/flow/gallery/deepsearch_tool_flow.py +39 -0
  28. flowllm/flow/gallery/expression_tool_flow.py +18 -0
  29. flowllm/flow/gallery/mock_tool_flow.py +67 -0
  30. flowllm/flow/gallery/tavily_search_tool_flow.py +30 -0
  31. flowllm/flow/gallery/terminate_tool_flow.py +30 -0
  32. flowllm/flow/parser/expression_parser.py +171 -0
  33. flowllm/llm/__init__.py +2 -0
  34. {llmflow → flowllm}/llm/base_llm.py +100 -18
  35. flowllm/llm/litellm_llm.py +455 -0
  36. flowllm/llm/openai_compatible_llm.py +439 -0
  37. flowllm/op/__init__.py +11 -0
  38. llmflow/op/react/react_v1_op.py → flowllm/op/agent/react_op.py +17 -22
  39. flowllm/op/akshare/__init__.py +3 -0
  40. flowllm/op/akshare/get_ak_a_code_op.py +108 -0
  41. flowllm/op/akshare/get_ak_a_code_prompt.yaml +21 -0
  42. flowllm/op/akshare/get_ak_a_info_op.py +140 -0
  43. flowllm/op/base_llm_op.py +64 -0
  44. flowllm/op/base_op.py +148 -0
  45. flowllm/op/base_ray_op.py +313 -0
  46. flowllm/op/code/__init__.py +1 -0
  47. flowllm/op/code/execute_code_op.py +42 -0
  48. flowllm/op/gallery/__init__.py +2 -0
  49. flowllm/op/gallery/mock_op.py +42 -0
  50. flowllm/op/gallery/terminate_op.py +29 -0
  51. flowllm/op/parallel_op.py +23 -0
  52. flowllm/op/search/__init__.py +3 -0
  53. flowllm/op/search/dashscope_deep_research_op.py +260 -0
  54. flowllm/op/search/dashscope_search_op.py +179 -0
  55. flowllm/op/search/dashscope_search_prompt.yaml +13 -0
  56. flowllm/op/search/tavily_search_op.py +102 -0
  57. flowllm/op/sequential_op.py +21 -0
  58. flowllm/schema/flow_request.py +12 -0
  59. flowllm/schema/flow_response.py +12 -0
  60. flowllm/schema/message.py +35 -0
  61. flowllm/schema/service_config.py +72 -0
  62. flowllm/schema/tool_call.py +118 -0
  63. {llmflow → flowllm}/schema/vector_node.py +1 -0
  64. flowllm/service/__init__.py +3 -0
  65. flowllm/service/base_service.py +68 -0
  66. flowllm/service/cmd_service.py +15 -0
  67. flowllm/service/http_service.py +79 -0
  68. flowllm/service/mcp_service.py +47 -0
  69. flowllm/storage/__init__.py +1 -0
  70. flowllm/storage/cache/__init__.py +1 -0
  71. flowllm/storage/cache/cache_data_handler.py +104 -0
  72. flowllm/storage/cache/data_cache.py +375 -0
  73. flowllm/storage/vector_store/__init__.py +3 -0
  74. flowllm/storage/vector_store/base_vector_store.py +44 -0
  75. {llmflow → flowllm/storage}/vector_store/chroma_vector_store.py +11 -10
  76. {llmflow → flowllm/storage}/vector_store/es_vector_store.py +11 -11
  77. llmflow/vector_store/file_vector_store.py → flowllm/storage/vector_store/local_vector_store.py +110 -11
  78. flowllm/utils/common_utils.py +52 -0
  79. flowllm/utils/fetch_url.py +117 -0
  80. flowllm/utils/llm_utils.py +28 -0
  81. flowllm/utils/ridge_v2.py +54 -0
  82. {llmflow → flowllm}/utils/timer.py +5 -4
  83. {flowllm-0.1.0.dist-info → flowllm-0.1.2.dist-info}/METADATA +45 -388
  84. flowllm-0.1.2.dist-info/RECORD +99 -0
  85. flowllm-0.1.2.dist-info/entry_points.txt +2 -0
  86. {flowllm-0.1.0.dist-info → flowllm-0.1.2.dist-info}/licenses/LICENSE +1 -1
  87. flowllm-0.1.2.dist-info/top_level.txt +1 -0
  88. flowllm-0.1.0.dist-info/RECORD +0 -66
  89. flowllm-0.1.0.dist-info/entry_points.txt +0 -3
  90. flowllm-0.1.0.dist-info/top_level.txt +0 -1
  91. llmflow/app.py +0 -53
  92. llmflow/config/config_parser.py +0 -80
  93. llmflow/config/mock_config.yaml +0 -58
  94. llmflow/embedding_model/__init__.py +0 -5
  95. llmflow/enumeration/agent_state.py +0 -8
  96. llmflow/llm/__init__.py +0 -5
  97. llmflow/llm/openai_compatible_llm.py +0 -283
  98. llmflow/mcp_server.py +0 -110
  99. llmflow/op/__init__.py +0 -10
  100. llmflow/op/base_op.py +0 -125
  101. llmflow/op/mock_op.py +0 -40
  102. llmflow/op/vector_store/__init__.py +0 -13
  103. llmflow/op/vector_store/recall_vector_store_op.py +0 -48
  104. llmflow/op/vector_store/update_vector_store_op.py +0 -28
  105. llmflow/op/vector_store/vector_store_action_op.py +0 -46
  106. llmflow/pipeline/pipeline.py +0 -94
  107. llmflow/pipeline/pipeline_context.py +0 -37
  108. llmflow/schema/app_config.py +0 -69
  109. llmflow/schema/experience.py +0 -144
  110. llmflow/schema/message.py +0 -68
  111. llmflow/schema/request.py +0 -32
  112. llmflow/schema/response.py +0 -29
  113. llmflow/service/__init__.py +0 -0
  114. llmflow/service/llmflow_service.py +0 -96
  115. llmflow/tool/__init__.py +0 -9
  116. llmflow/tool/base_tool.py +0 -80
  117. llmflow/tool/code_tool.py +0 -43
  118. llmflow/tool/dashscope_search_tool.py +0 -162
  119. llmflow/tool/mcp_tool.py +0 -77
  120. llmflow/tool/tavily_search_tool.py +0 -109
  121. llmflow/tool/terminate_tool.py +0 -23
  122. llmflow/utils/__init__.py +0 -0
  123. llmflow/utils/common_utils.py +0 -17
  124. llmflow/utils/file_handler.py +0 -25
  125. llmflow/utils/http_client.py +0 -156
  126. llmflow/utils/op_utils.py +0 -102
  127. llmflow/utils/registry.py +0 -33
  128. llmflow/vector_store/__init__.py +0 -7
  129. llmflow/vector_store/base_vector_store.py +0 -136
  130. {llmflow → flowllm/context}/__init__.py +0 -0
  131. {llmflow/config → flowllm/enumeration}/__init__.py +0 -0
  132. {llmflow → flowllm}/enumeration/chunk_enum.py +0 -0
  133. {llmflow → flowllm}/enumeration/http_enum.py +0 -0
  134. {llmflow → flowllm}/enumeration/role.py +0 -0
  135. {llmflow/enumeration → flowllm/flow/parser}/__init__.py +0 -0
  136. {llmflow/op/react → flowllm/op/agent}/__init__.py +0 -0
  137. /llmflow/op/react/react_v1_prompt.yaml → /flowllm/op/agent/react_prompt.yaml +0 -0
  138. {llmflow/pipeline → flowllm/schema}/__init__.py +0 -0
  139. {llmflow/schema → flowllm/utils}/__init__.py +0 -0
  140. {llmflow → flowllm}/utils/singleton.py +0 -0
  141. {flowllm-0.1.0.dist-info → flowllm-0.1.2.dist-info}/WHEEL +0 -0
@@ -0,0 +1,455 @@
1
+ import asyncio
2
+ import os
3
+ from typing import List, Dict
4
+
5
+ from litellm import completion, acompletion
6
+ from loguru import logger
7
+ from pydantic import Field, PrivateAttr, model_validator
8
+
9
+ from flowllm.context.service_context import C
10
+ from flowllm.enumeration.chunk_enum import ChunkEnum
11
+ from flowllm.enumeration.role import Role
12
+ from flowllm.llm.base_llm import BaseLLM
13
+ from flowllm.schema.message import Message
14
+ from flowllm.schema.tool_call import ToolCall
15
+
16
+
17
+ @C.register_llm("litellm")
18
+ class LiteLLMBaseLLM(BaseLLM):
19
+ """
20
+ LiteLLM-compatible LLM implementation supporting multiple LLM providers through unified interface.
21
+
22
+ This class implements the BaseLLM interface using LiteLLM, which provides:
23
+ - Support for 100+ LLM providers (OpenAI, Anthropic, Cohere, Azure, etc.)
24
+ - Streaming responses with different chunk types (content, tools, usage)
25
+ - Tool calling with parallel execution support
26
+ - Unified API across different providers
27
+ - Robust error handling and retries
28
+
29
+ LiteLLM automatically handles provider-specific authentication and request formatting.
30
+ """
31
+
32
+ # API configuration - LiteLLM handles provider-specific settings
33
+ api_key: str = Field(default_factory=lambda: os.getenv("FLOW_LLM_API_KEY"),
34
+ description="API key for authentication")
35
+ base_url: str = Field(default_factory=lambda: os.getenv("FLOW_LLM_BASE_URL"),
36
+ description="Base URL for custom endpoints")
37
+
38
+ # LiteLLM specific configuration
39
+ custom_llm_provider: str = Field(default="openai", description="Custom LLM provider name for LiteLLM routing")
40
+
41
+ # Additional LiteLLM parameters
42
+ timeout: float = Field(default=600, description="Request timeout in seconds")
43
+ max_tokens: int = Field(default=None, description="Maximum tokens to generate")
44
+
45
+ # Private attributes for LiteLLM configuration
46
+ _litellm_params: dict = PrivateAttr(default_factory=dict)
47
+
48
+ @model_validator(mode="after")
49
+ def init_litellm_config(self):
50
+ """
51
+ Initialize LiteLLM configuration after model validation.
52
+
53
+ This validator sets up LiteLLM-specific parameters and environment variables
54
+ required for different providers. It configures authentication and routing
55
+ based on the model name and provider settings.
56
+
57
+ Returns:
58
+ Self for method chaining
59
+ """
60
+
61
+ # Configure LiteLLM parameters
62
+ self._litellm_params = {
63
+ "api_key": self.api_key,
64
+ "base_url": self.base_url, #.replace("/v1", "")
65
+ "model": self.model_name,
66
+ "temperature": self.temperature,
67
+ "seed": self.seed,
68
+ "timeout": self.timeout,
69
+ }
70
+
71
+ # Add optional parameters
72
+ if self.top_p is not None:
73
+ self._litellm_params["top_p"] = self.top_p
74
+ if self.max_tokens is not None:
75
+ self._litellm_params["max_tokens"] = self.max_tokens
76
+ if self.presence_penalty is not None:
77
+ self._litellm_params["presence_penalty"] = self.presence_penalty
78
+ if self.custom_llm_provider:
79
+ self._litellm_params["custom_llm_provider"] = self.custom_llm_provider
80
+
81
+ return self
82
+
83
+ def stream_chat(self, messages: List[Message], tools: List[ToolCall] = None, **kwargs):
84
+ """
85
+ Stream chat completions from LiteLLM with support for multiple providers.
86
+
87
+ This method handles streaming responses and categorizes chunks into different types:
88
+ - ANSWER: Regular response content from the model
89
+ - TOOL: Tool calls that need to be executed
90
+ - USAGE: Token usage statistics (when available)
91
+ - ERROR: Error information from failed requests
92
+
93
+ Args:
94
+ messages: List of conversation messages
95
+ tools: Optional list of tools available to the model
96
+ **kwargs: Additional parameters passed to LiteLLM
97
+
98
+ Yields:
99
+ Tuple of (chunk_content, ChunkEnum) for each streaming piece
100
+ """
101
+ for i in range(self.max_retries):
102
+ try:
103
+ # Prepare parameters for LiteLLM
104
+ params = self._litellm_params.copy()
105
+ params.update(kwargs)
106
+ params.update({
107
+ "messages": [x.simple_dump() for x in messages],
108
+ "stream": True,
109
+ })
110
+
111
+ # Add tools if provided
112
+ if tools:
113
+ params["tools"] = [x.simple_input_dump() for x in tools]
114
+ params["tool_choice"] = self.tool_choice if self.tool_choice else "auto"
115
+
116
+ # Create streaming completion using LiteLLM
117
+ completion_response = completion(**params)
118
+
119
+ # Initialize tool call tracking
120
+ ret_tools: List[ToolCall] = [] # Accumulate tool calls across chunks
121
+
122
+ # Process each chunk in the streaming response
123
+ for chunk in completion_response:
124
+ try:
125
+ # Handle chunks without choices (usually usage/metadata)
126
+ if not hasattr(chunk, 'choices') or not chunk.choices:
127
+ # Check for usage information
128
+ if hasattr(chunk, 'usage') and chunk.usage:
129
+ yield chunk.usage, ChunkEnum.USAGE
130
+ continue
131
+
132
+ delta = chunk.choices[0].delta
133
+
134
+ # Handle regular response content
135
+ if hasattr(delta, 'content') and delta.content is not None:
136
+ yield delta.content, ChunkEnum.ANSWER
137
+
138
+ # Handle tool calls (function calling)
139
+ if hasattr(delta, 'tool_calls') and delta.tool_calls is not None:
140
+ for tool_call in delta.tool_calls:
141
+ index = getattr(tool_call, 'index', 0)
142
+
143
+ # Ensure we have enough tool call slots
144
+ while len(ret_tools) <= index:
145
+ ret_tools.append(ToolCall(index=index))
146
+
147
+ # Accumulate tool call information across chunks
148
+ if hasattr(tool_call, 'id') and tool_call.id:
149
+ ret_tools[index].id += tool_call.id
150
+
151
+ if (hasattr(tool_call, 'function') and tool_call.function and
152
+ hasattr(tool_call.function, 'name') and tool_call.function.name):
153
+ ret_tools[index].name += tool_call.function.name
154
+
155
+ if (hasattr(tool_call, 'function') and tool_call.function and
156
+ hasattr(tool_call.function, 'arguments') and tool_call.function.arguments):
157
+ ret_tools[index].arguments += tool_call.function.arguments
158
+
159
+ except Exception as chunk_error:
160
+ logger.warning(f"Error processing chunk: {chunk_error}")
161
+ continue
162
+
163
+ # Yield completed tool calls after streaming finishes
164
+ if ret_tools:
165
+ tool_dict: Dict[str, ToolCall] = {x.name: x for x in tools} if tools else {}
166
+ for tool in ret_tools:
167
+ # Only yield tool calls that correspond to available tools
168
+ if tools and tool.name not in tool_dict:
169
+ continue
170
+
171
+ yield tool, ChunkEnum.TOOL
172
+
173
+ return
174
+
175
+ except Exception as e:
176
+ logger.exception(f"stream chat with LiteLLM model={self.model_name} encounter error: {e}")
177
+
178
+ # Handle retry logic
179
+ if i == self.max_retries - 1 and self.raise_exception:
180
+ raise e
181
+ else:
182
+ error_msg = str(e.args) if hasattr(e, 'args') else str(e)
183
+ yield error_msg, ChunkEnum.ERROR
184
+
185
+ async def astream_chat(self, messages: List[Message], tools: List[ToolCall] = None, **kwargs):
186
+ """
187
+ Async stream chat completions from LiteLLM with support for multiple providers.
188
+
189
+ This method handles async streaming responses and categorizes chunks into different types:
190
+ - ANSWER: Regular response content from the model
191
+ - TOOL: Tool calls that need to be executed
192
+ - USAGE: Token usage statistics (when available)
193
+ - ERROR: Error information from failed requests
194
+
195
+ Args:
196
+ messages: List of conversation messages
197
+ tools: Optional list of tools available to the model
198
+ **kwargs: Additional parameters passed to LiteLLM
199
+
200
+ Yields:
201
+ Tuple of (chunk_content, ChunkEnum) for each streaming piece
202
+ """
203
+ for i in range(self.max_retries):
204
+ try:
205
+ # Prepare parameters for LiteLLM
206
+ params = self._litellm_params.copy()
207
+ params.update(kwargs)
208
+ params.update({
209
+ "messages": [x.simple_dump() for x in messages],
210
+ "stream": True,
211
+ })
212
+
213
+ # Add tools if provided
214
+ if tools:
215
+ params["tools"] = [x.simple_input_dump() for x in tools]
216
+ params["tool_choice"] = self.tool_choice if self.tool_choice else "auto"
217
+
218
+ # Create async streaming completion using LiteLLM
219
+ completion_response = await acompletion(**params)
220
+
221
+ # Initialize tool call tracking
222
+ ret_tools: List[ToolCall] = [] # Accumulate tool calls across chunks
223
+
224
+ # Process each chunk in the async streaming response
225
+ async for chunk in completion_response:
226
+ try:
227
+ # Handle chunks without choices (usually usage/metadata)
228
+ if not hasattr(chunk, 'choices') or not chunk.choices:
229
+ # Check for usage information
230
+ if hasattr(chunk, 'usage') and chunk.usage:
231
+ yield chunk.usage, ChunkEnum.USAGE
232
+ continue
233
+
234
+ delta = chunk.choices[0].delta
235
+
236
+ # Handle regular response content
237
+ if hasattr(delta, 'content') and delta.content is not None:
238
+ yield delta.content, ChunkEnum.ANSWER
239
+
240
+ # Handle tool calls (function calling)
241
+ if hasattr(delta, 'tool_calls') and delta.tool_calls is not None:
242
+ for tool_call in delta.tool_calls:
243
+ index = getattr(tool_call, 'index', 0)
244
+
245
+ # Ensure we have enough tool call slots
246
+ while len(ret_tools) <= index:
247
+ ret_tools.append(ToolCall(index=index))
248
+
249
+ # Accumulate tool call information across chunks
250
+ if hasattr(tool_call, 'id') and tool_call.id:
251
+ ret_tools[index].id += tool_call.id
252
+
253
+ if (hasattr(tool_call, 'function') and tool_call.function and
254
+ hasattr(tool_call.function, 'name') and tool_call.function.name):
255
+ ret_tools[index].name += tool_call.function.name
256
+
257
+ if (hasattr(tool_call, 'function') and tool_call.function and
258
+ hasattr(tool_call.function, 'arguments') and tool_call.function.arguments):
259
+ ret_tools[index].arguments += tool_call.function.arguments
260
+
261
+ except Exception as chunk_error:
262
+ logger.warning(f"Error processing async chunk: {chunk_error}")
263
+ continue
264
+
265
+ # Yield completed tool calls after streaming finishes
266
+ if ret_tools:
267
+ tool_dict: Dict[str, ToolCall] = {x.name: x for x in tools} if tools else {}
268
+ for tool in ret_tools:
269
+ # Only yield tool calls that correspond to available tools
270
+ if tools and tool.name not in tool_dict:
271
+ continue
272
+
273
+ yield tool, ChunkEnum.TOOL
274
+
275
+ return
276
+
277
+ except Exception as e:
278
+ logger.exception(f"async stream chat with LiteLLM model={self.model_name} encounter error: {e}")
279
+
280
+ # Handle retry logic with async sleep
281
+ await asyncio.sleep(1 + i)
282
+
283
+ if i == self.max_retries - 1 and self.raise_exception:
284
+ raise e
285
+ else:
286
+ error_msg = str(e.args) if hasattr(e, 'args') else str(e)
287
+ yield error_msg, ChunkEnum.ERROR
288
+
289
+ def _chat(self, messages: List[Message], tools: List[ToolCall] = None, enable_stream_print: bool = False,
290
+ **kwargs) -> Message:
291
+ """
292
+ Perform a complete chat completion by aggregating streaming chunks from LiteLLM.
293
+
294
+ This method consumes the entire streaming response and combines all
295
+ chunks into a single Message object. It separates regular answer content
296
+ and tool calls, providing a complete response.
297
+
298
+ Args:
299
+ messages: List of conversation messages
300
+ tools: Optional list of tools available to the model
301
+ enable_stream_print: Whether to print streaming response to console
302
+ **kwargs: Additional parameters passed to LiteLLM
303
+
304
+ Returns:
305
+ Complete Message with all content aggregated
306
+ """
307
+ answer_content = "" # Final response content
308
+ tool_calls = [] # List of tool calls to execute
309
+
310
+ # Consume streaming response and aggregate chunks by type
311
+ for chunk, chunk_enum in self.stream_chat(messages, tools, **kwargs):
312
+ if chunk_enum is ChunkEnum.USAGE:
313
+ # Display token usage statistics
314
+ if enable_stream_print:
315
+ if hasattr(chunk, 'model_dump_json'):
316
+ print(f"\n<usage>{chunk.model_dump_json(indent=2)}</usage>")
317
+ else:
318
+ print(f"\n<usage>{chunk}</usage>")
319
+
320
+ elif chunk_enum is ChunkEnum.ANSWER:
321
+ if enable_stream_print:
322
+ print(chunk, end="")
323
+ answer_content += chunk
324
+
325
+ elif chunk_enum is ChunkEnum.TOOL:
326
+ if enable_stream_print:
327
+ if hasattr(chunk, 'model_dump_json'):
328
+ print(f"\n<tool>{chunk.model_dump_json()}</tool>", end="")
329
+ else:
330
+ print(f"\n<tool>{chunk}</tool>", end="")
331
+ tool_calls.append(chunk)
332
+
333
+ elif chunk_enum is ChunkEnum.ERROR:
334
+ if enable_stream_print:
335
+ print(f"\n<error>{chunk}</error>", end="")
336
+
337
+ # Construct complete response message
338
+ return Message(
339
+ role=Role.ASSISTANT,
340
+ content=answer_content,
341
+ tool_calls=tool_calls
342
+ )
343
+
344
+ async def _achat(self, messages: List[Message], tools: List[ToolCall] = None, enable_stream_print: bool = False,
345
+ **kwargs) -> Message:
346
+ """
347
+ Perform an async complete chat completion by aggregating streaming chunks from LiteLLM.
348
+
349
+ This method consumes the entire async streaming response and combines all
350
+ chunks into a single Message object. It separates regular answer content
351
+ and tool calls, providing a complete response.
352
+
353
+ Args:
354
+ messages: List of conversation messages
355
+ tools: Optional list of tools available to the model
356
+ enable_stream_print: Whether to print streaming response to console
357
+ **kwargs: Additional parameters passed to LiteLLM
358
+
359
+ Returns:
360
+ Complete Message with all content aggregated
361
+ """
362
+ answer_content = "" # Final response content
363
+ tool_calls = [] # List of tool calls to execute
364
+
365
+ # Consume async streaming response and aggregate chunks by type
366
+ async for chunk, chunk_enum in self.astream_chat(messages, tools, **kwargs):
367
+ if chunk_enum is ChunkEnum.USAGE:
368
+ # Display token usage statistics
369
+ if enable_stream_print:
370
+ if hasattr(chunk, 'model_dump_json'):
371
+ print(f"\n<usage>{chunk.model_dump_json(indent=2)}</usage>")
372
+ else:
373
+ print(f"\n<usage>{chunk}</usage>")
374
+
375
+ elif chunk_enum is ChunkEnum.ANSWER:
376
+ if enable_stream_print:
377
+ print(chunk, end="")
378
+ answer_content += chunk
379
+
380
+ elif chunk_enum is ChunkEnum.TOOL:
381
+ if enable_stream_print:
382
+ if hasattr(chunk, 'model_dump_json'):
383
+ print(f"\n<tool>{chunk.model_dump_json()}</tool>", end="")
384
+ else:
385
+ print(f"\n<tool>{chunk}</tool>", end="")
386
+ tool_calls.append(chunk)
387
+
388
+ elif chunk_enum is ChunkEnum.ERROR:
389
+ if enable_stream_print:
390
+ print(f"\n<error>{chunk}</error>", end="")
391
+
392
+ # Construct complete response message
393
+ return Message(
394
+ role=Role.ASSISTANT,
395
+ content=answer_content,
396
+ tool_calls=tool_calls
397
+ )
398
+
399
+
400
+ async def async_main():
401
+ """
402
+ Async test function for LiteLLMBaseLLM.
403
+
404
+ This function demonstrates how to use the LiteLLMBaseLLM class
405
+ with async operations. It requires proper environment variables
406
+ to be set for the chosen LLM provider.
407
+ """
408
+ from flowllm.utils.common_utils import load_env
409
+
410
+ load_env()
411
+
412
+ # Example with OpenAI model through LiteLLM
413
+ model_name = "qwen-max-2025-01-25" # LiteLLM will route to OpenAI
414
+ llm = LiteLLMBaseLLM(model_name=model_name)
415
+
416
+ # Test async chat
417
+ message: Message = await llm.achat(
418
+ [Message(role=Role.USER, content="Hello! How are you?")],
419
+ [],
420
+ enable_stream_print=True
421
+ )
422
+ print("\nAsync result:", message)
423
+
424
+
425
+ def main():
426
+ """
427
+ Sync test function for LiteLLMBaseLLM.
428
+
429
+ This function demonstrates how to use the LiteLLMBaseLLM class
430
+ with synchronous operations. It requires proper environment variables
431
+ to be set for the chosen LLM provider.
432
+ """
433
+ from flowllm.utils.common_utils import load_env
434
+
435
+ load_env()
436
+
437
+ # Example with OpenAI model through LiteLLM
438
+ model_name = "qwen-max-2025-01-25" # LiteLLM will route to OpenAI
439
+ llm = LiteLLMBaseLLM(model_name=model_name)
440
+
441
+ # Test sync chat
442
+ message: Message = llm.chat(
443
+ [Message(role=Role.USER, content="Hello! How are you?")],
444
+ [],
445
+ enable_stream_print=True
446
+ )
447
+ print("\nSync result:", message)
448
+
449
+
450
+ if __name__ == "__main__":
451
+ main()
452
+
453
+ # import asyncio
454
+ #
455
+ # asyncio.run(async_main())