SimpleLLMFunc 0.2.0__tar.gz → 0.2.3__tar.gz

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 (26) hide show
  1. {simplellmfunc-0.2.0 → simplellmfunc-0.2.3}/PKG-INFO +4 -4
  2. {simplellmfunc-0.2.0 → simplellmfunc-0.2.3}/README.md +2 -3
  3. simplellmfunc-0.2.3/SimpleLLMFunc/.DS_Store +0 -0
  4. {simplellmfunc-0.2.0 → simplellmfunc-0.2.3}/SimpleLLMFunc/interface/llm_interface.py +16 -11
  5. {simplellmfunc-0.2.0 → simplellmfunc-0.2.3}/SimpleLLMFunc/interface/openai_compatible.py +108 -58
  6. {simplellmfunc-0.2.0 → simplellmfunc-0.2.3}/SimpleLLMFunc/llm_decorator/__init__.py +5 -3
  7. simplellmfunc-0.2.3/SimpleLLMFunc/llm_decorator/llm_chat_decorator.py +814 -0
  8. simplellmfunc-0.2.3/SimpleLLMFunc/llm_decorator/llm_function_decorator.py +1146 -0
  9. simplellmfunc-0.2.3/SimpleLLMFunc/llm_decorator/multimodal_types.py +110 -0
  10. {simplellmfunc-0.2.0 → simplellmfunc-0.2.3}/SimpleLLMFunc/llm_decorator/utils.py +171 -149
  11. {simplellmfunc-0.2.0 → simplellmfunc-0.2.3}/SimpleLLMFunc/logger/__init__.py +2 -0
  12. {simplellmfunc-0.2.0 → simplellmfunc-0.2.3}/SimpleLLMFunc/logger/logger.py +151 -28
  13. simplellmfunc-0.2.3/SimpleLLMFunc/type/__init__.py +9 -0
  14. simplellmfunc-0.2.3/SimpleLLMFunc/utils.py +23 -0
  15. {simplellmfunc-0.2.0 → simplellmfunc-0.2.3}/pyproject.toml +2 -1
  16. simplellmfunc-0.2.0/SimpleLLMFunc/llm_decorator/llm_chat_decorator.py +0 -331
  17. simplellmfunc-0.2.0/SimpleLLMFunc/llm_decorator/llm_function_decorator.py +0 -473
  18. simplellmfunc-0.2.0/SimpleLLMFunc/utils.py +0 -14
  19. {simplellmfunc-0.2.0 → simplellmfunc-0.2.3}/LICENSE +0 -0
  20. {simplellmfunc-0.2.0 → simplellmfunc-0.2.3}/SimpleLLMFunc/__init__.py +0 -0
  21. {simplellmfunc-0.2.0 → simplellmfunc-0.2.3}/SimpleLLMFunc/config.py +0 -0
  22. {simplellmfunc-0.2.0 → simplellmfunc-0.2.3}/SimpleLLMFunc/interface/__init__.py +0 -0
  23. {simplellmfunc-0.2.0 → simplellmfunc-0.2.3}/SimpleLLMFunc/interface/key_pool.py +0 -0
  24. {simplellmfunc-0.2.0 → simplellmfunc-0.2.3}/SimpleLLMFunc/logger/logger_config.py +0 -0
  25. {simplellmfunc-0.2.0 → simplellmfunc-0.2.3}/SimpleLLMFunc/tool/__init__.py +0 -0
  26. {simplellmfunc-0.2.0 → simplellmfunc-0.2.3}/SimpleLLMFunc/tool/tool.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: SimpleLLMFunc
3
- Version: 0.2.0
3
+ Version: 0.2.3
4
4
  Summary: 一个轻量但完备的LLM/Agent应用开发框架,提供装饰器实现将函数DocString作为Prompt而无需函数体具体实现但能够享受函数定义和类型标注带来效率提升的开发体验。以最Code的方式,用最少的代码将LLM能力集成到任意Python项目中。
5
5
  Author: Ni Jingzhe
6
6
  Author-email: nijingzhe@zjue.edu.cn
@@ -15,10 +15,10 @@ Requires-Dist: openai (>=1.84.0,<2.0.0)
15
15
  Requires-Dist: pydantic (>=2.11.5,<3.0.0)
16
16
  Requires-Dist: pydantic-settings (>=2.9.1,<3.0.0)
17
17
  Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
18
+ Requires-Dist: rich (>=14.0.0,<15.0.0)
18
19
  Description-Content-Type: text/markdown
19
20
 
20
-
21
- ![SimpleLLMFunc](https://github.com/NiJingzhe/SimpleLLMFunc/blob/master/img/repocover.png?raw=true)
21
+ ![SimpleLLMFunc](https://github.com/NiJingzhe/SimpleLLMFunc/blob/master/img/repocover_new.png?raw=true)
22
22
 
23
23
  <center>
24
24
  <h2 style="font-size:2em;">Everything is Function, Prompt is Code</h2>
@@ -480,7 +480,7 @@ pip install SimpleLLMFunc
480
480
  month = {June},
481
481
  title = {{SimpleLLMFunc: A New Approach to Build LLM Applications}},
482
482
  url = {https://github.com/NiJingzhe/SimpleLLMFunc},
483
- version = {0.2.0},
483
+ version = {0.2.1},
484
484
  year = {2025}
485
485
  }
486
486
  ```
@@ -1,5 +1,4 @@
1
-
2
- ![SimpleLLMFunc](https://github.com/NiJingzhe/SimpleLLMFunc/blob/master/img/repocover.png?raw=true)
1
+ ![SimpleLLMFunc](https://github.com/NiJingzhe/SimpleLLMFunc/blob/master/img/repocover_new.png?raw=true)
3
2
 
4
3
  <center>
5
4
  <h2 style="font-size:2em;">Everything is Function, Prompt is Code</h2>
@@ -461,7 +460,7 @@ pip install SimpleLLMFunc
461
460
  month = {June},
462
461
  title = {{SimpleLLMFunc: A New Approach to Build LLM Applications}},
463
462
  url = {https://github.com/NiJingzhe/SimpleLLMFunc},
464
- version = {0.2.0},
463
+ version = {0.2.1},
465
464
  year = {2025}
466
465
  }
467
466
  ```
@@ -1,19 +1,22 @@
1
1
  from abc import ABC, abstractmethod
2
- from typing import Generator, Union, Optional, Dict, List, Iterable, Literal, Any
2
+ from typing import Optional, Dict, Iterable, Literal, Any, AsyncGenerator
3
3
 
4
4
  from SimpleLLMFunc.interface.key_pool import APIKeyPool
5
5
  from SimpleLLMFunc.logger import get_current_trace_id
6
6
 
7
+
7
8
  class LLM_Interface(ABC):
8
-
9
+
9
10
  @abstractmethod
10
- def __init__(self, api_key_pool: APIKeyPool, model_name: str, base_url: Optional[str] = None):
11
+ def __init__(
12
+ self, api_key_pool: APIKeyPool, model_name: str, base_url: Optional[str] = None
13
+ ):
11
14
  self.input_token_count = 0
12
15
  self.output_token_count = 0
13
-
16
+
14
17
  @abstractmethod
15
- def chat(
16
- self,
18
+ async def chat(
19
+ self,
17
20
  trace_id: str = get_current_trace_id(),
18
21
  stream: Literal[False] = False,
19
22
  messages: Iterable[Dict[str, str]] = [{"role": "user", "content": ""}],
@@ -22,15 +25,17 @@ class LLM_Interface(ABC):
22
25
  **kwargs,
23
26
  ) -> Dict[Any, Any]:
24
27
  pass
25
-
28
+
26
29
  @abstractmethod
27
- def chat_stream(
28
- self,
30
+ async def chat_stream(
31
+ self,
29
32
  trace_id: str = get_current_trace_id(),
30
33
  stream: Literal[True] = True,
31
34
  messages: Iterable[Dict[str, str]] = [{"role": "user", "content": ""}],
32
35
  timeout: Optional[int] = None,
33
36
  *args,
34
37
  **kwargs,
35
- ) -> Generator[Dict[Any, Any], None, None]:
36
- pass
38
+ ) -> AsyncGenerator[Dict[Any, Any], None]:
39
+ # 空的异步生成器,永远不会产生任何值
40
+ if False:
41
+ yield {}
@@ -2,13 +2,25 @@ from __future__ import annotations
2
2
  import json
3
3
  import os
4
4
  import time
5
- from typing import Generator, Optional, Dict, List, Literal, Iterable, Any
5
+ from typing import Optional, Dict, Literal, Iterable, Any, AsyncGenerator
6
6
 
7
- from openai import OpenAI, AsyncOpenAI
7
+ from openai import AsyncOpenAI
8
8
  from SimpleLLMFunc.interface.llm_interface import LLM_Interface
9
9
  from SimpleLLMFunc.interface.key_pool import APIKeyPool
10
- from SimpleLLMFunc.logger import app_log, push_warning, push_error, get_location, get_current_trace_id
11
- from SimpleLLMFunc.logger.logger import push_critical, get_current_context_attribute, set_current_context_attribute
10
+ from SimpleLLMFunc.logger import (
11
+ app_log,
12
+ push_warning,
13
+ push_error,
14
+ get_location,
15
+ get_current_trace_id,
16
+ push_debug,
17
+ )
18
+ from SimpleLLMFunc.logger.logger import (
19
+ push_critical,
20
+ get_current_context_attribute,
21
+ set_current_context_attribute,
22
+ )
23
+
12
24
 
13
25
  class OpenAICompatible(LLM_Interface):
14
26
  """与OpenAI API兼容的LLM接口实现,支持任何符合OpenAI格式的API接口。
@@ -19,10 +31,10 @@ class OpenAICompatible(LLM_Interface):
19
31
 
20
32
  def _count_tokens(self, response: Any) -> tuple[int, int]:
21
33
  """计算响应中的token数量
22
-
34
+
23
35
  Args:
24
36
  response: OpenAI API的响应对象
25
-
37
+
26
38
  Returns:
27
39
  (输入token数, 输出token数)的元组
28
40
  """
@@ -35,12 +47,14 @@ class OpenAICompatible(LLM_Interface):
35
47
  return 0, 0
36
48
 
37
49
  @classmethod
38
- def load_from_json_file(cls, json_path: str) -> Dict[str, Dict[str, OpenAICompatible]]:
50
+ def load_from_json_file(
51
+ cls, json_path: str
52
+ ) -> Dict[str, Dict[str, OpenAICompatible]]:
39
53
  """从JSON字符串加载OpenAICompatible实例
40
54
 
41
55
  Args:
42
56
  json_str: JSON字符串,包含API密钥和模型名称
43
-
57
+
44
58
  例如:
45
59
  ```
46
60
  {
@@ -75,25 +89,28 @@ class OpenAICompatible(LLM_Interface):
75
89
  "max_retries": 5,
76
90
  "retry_delay": 1.0
77
91
  }
78
- ]
92
+ ]
79
93
  }
80
94
  ```
81
95
 
82
96
  Returns:
83
- OpenAICompatible实例的字典, 可以这样访问:
97
+ OpenAICompatible实例的字典, 可以这样访问:
84
98
  ```python
85
99
  from SimpleLLMFunc.interface.openai_compatible import OpenAICompatible
86
100
 
87
101
  all_models = OpenAICompatible.load_from_json(json_str)
88
102
  gpt_3_5 = all_models["openai"]["gpt-3.5-turbo"]
89
103
  gpt_4 = all_models["openai"]["gpt-4"]
90
- ```
104
+ ```
91
105
  """
92
-
106
+
93
107
  if not os.path.exists(json_path):
94
- push_critical(f"JSON 文件 {json_path} 不存在。请检查您的配置。", location=get_location())
108
+ push_critical(
109
+ f"JSON 文件 {json_path} 不存在。请检查您的配置。",
110
+ location=get_location(),
111
+ )
95
112
  raise FileNotFoundError(f"JSON 文件 {json_path} 不存在。")
96
-
113
+
97
114
  with open(json_path, "r", encoding="utf-8") as f:
98
115
  json_str = f.read()
99
116
  # 解析JSON字符串
@@ -103,20 +120,25 @@ class OpenAICompatible(LLM_Interface):
103
120
  push_critical(f"解析 JSON 字符串失败:{e}", location=get_location())
104
121
  raise ValueError(f"解析 JSON 字符串失败:{e}")
105
122
  # 检查JSON格式
106
-
107
- try:
123
+
124
+ try:
108
125
  all_providers_dict: Dict[str, Dict[str, OpenAICompatible]] = {}
109
126
  for provider_id, models in all_providers_info.items():
110
127
  all_providers_dict[provider_id] = {}
111
128
  app_log(
112
129
  f"正在为提供商加载 OpenAICompatible 实例:{provider_id}",
113
- location=get_location()
130
+ location=get_location(),
114
131
  )
115
-
132
+
116
133
  if not isinstance(models, list):
117
- push_critical(f"提供商 {provider_id} 下的模型格式无效。应为列表。", location=get_location())
118
- raise TypeError(f"提供商 {provider_id} 下的模型格式无效。应为列表。")
119
-
134
+ push_critical(
135
+ f"提供商 {provider_id} 下的模型格式无效。应为列表。",
136
+ location=get_location(),
137
+ )
138
+ raise TypeError(
139
+ f"提供商 {provider_id} 下的模型格式无效。应为列表。"
140
+ )
141
+
120
142
  for model_info in models:
121
143
  model_name = model_info["model_name"]
122
144
  api_keys = model_info["api_keys"]
@@ -137,13 +159,15 @@ class OpenAICompatible(LLM_Interface):
137
159
  )
138
160
 
139
161
  all_providers_dict[provider_id][model_name] = instance
140
-
162
+
141
163
  app_log(
142
164
  f"已为提供商 {provider_id} 加载模型 {model_name} 的 OpenAICompatible 实例",
143
- location=get_location()
165
+ location=get_location(),
144
166
  )
145
167
  except ValueError as e:
146
- push_critical(f"加载 OpenAICompatible 实例时出错:{e}", location=get_location())
168
+ push_critical(
169
+ f"加载 OpenAICompatible 实例时出错:{e}", location=get_location()
170
+ )
147
171
  raise ValueError(f"加载 OpenAICompatible 实例时出错:{e}")
148
172
  except TypeError as e:
149
173
  push_critical(f"JSON 中的类型无效:{e}", location=get_location())
@@ -152,14 +176,19 @@ class OpenAICompatible(LLM_Interface):
152
176
  push_critical(f"JSON 中缺少必需的密钥:{e}", location=get_location())
153
177
  raise ValueError(f"JSON 中缺少必需的密钥:{e}")
154
178
  except Exception as e:
155
- push_critical(f"加载 OpenAICompatible 实例时发生未知错误:{e}", location=get_location())
179
+ push_critical(
180
+ f"加载 OpenAICompatible 实例时发生未知错误:{e}",
181
+ location=get_location(),
182
+ )
156
183
  raise ValueError(f"加载 OpenAICompatible 实例时发生未知错误:{e}")
157
-
184
+
158
185
  return all_providers_dict
159
186
 
160
187
  def __repr__(self) -> str:
161
188
  """返回OpenAICompatible实例的字符串表示"""
162
- return f"OpenAICompatible(model_name={self.model_name}, base_url={self.base_url})"
189
+ return (
190
+ f"OpenAICompatible(model_name={self.model_name}, base_url={self.base_url})"
191
+ )
163
192
 
164
193
  def __init__(
165
194
  self,
@@ -186,13 +215,20 @@ class OpenAICompatible(LLM_Interface):
186
215
  self.model_name = model_name
187
216
 
188
217
  self.key_pool = api_key_pool
189
- self.client = OpenAI(api_key=api_key_pool.get_least_loaded_key(), base_url=self.base_url)
218
+ self.client = AsyncOpenAI(
219
+ api_key=api_key_pool.get_least_loaded_key(), base_url=self.base_url
220
+ )
190
221
 
191
- def chat(
222
+ async def chat(
192
223
  self,
193
224
  trace_id: str = get_current_trace_id(),
194
225
  stream: Literal[False] = False,
195
- messages: Iterable[Dict[str, str]] = [{"role": "system", "content": "你是一位乐于助人的助手,可以帮助用户解决各种问题。"}],
226
+ messages: Iterable[Dict[str, str]] = [
227
+ {
228
+ "role": "system",
229
+ "content": "你是一位乐于助人的助手,可以帮助用户解决各种问题。",
230
+ }
231
+ ],
196
232
  timeout: Optional[int] = 30,
197
233
  *args,
198
234
  **kwargs,
@@ -210,19 +246,19 @@ class OpenAICompatible(LLM_Interface):
210
246
  LLM的响应内容
211
247
  """
212
248
  key = self.key_pool.get_least_loaded_key()
213
- self.client = OpenAI(api_key=key, base_url=self.base_url)
214
-
249
+ self.client = AsyncOpenAI(api_key=key, base_url=self.base_url)
250
+
215
251
  attempt = 0
216
252
  while attempt < self.max_retries:
217
253
  try:
218
254
  self.key_pool.increment_task_count(key)
219
255
  data = json.dumps(messages, ensure_ascii=False, indent=4)
220
- app_log(
256
+ push_debug(
221
257
  f"OpenAICompatible::chat: {self.model_name} request with API key: {key}, and message: {data}",
222
- location=get_location()
258
+ location=get_location(),
223
259
  )
224
- response: Dict[Any, Any] = self.client.chat.completions.create( # type: ignore
225
- messages=messages, # type: ignore
260
+ response: Dict[Any, Any] = await self.client.chat.completions.create( # type: ignore
261
+ messages=messages, # type: ignore
226
262
  model=self.model_name,
227
263
  stream=stream,
228
264
  timeout=timeout,
@@ -233,17 +269,21 @@ class OpenAICompatible(LLM_Interface):
233
269
  # 统计token
234
270
  if not (response.choices and response.choices[0].message and response.choices[0].message.tool_calls): # type: ignore
235
271
  prompt_tokens, completion_tokens = self._count_tokens(response)
236
-
272
+
237
273
  # 更新上下文中的token计数
238
274
  input_tokens = get_current_context_attribute("input_tokens") or 0
239
275
  output_tokens = get_current_context_attribute("output_tokens") or 0
240
-
241
- set_current_context_attribute("input_tokens", input_tokens + prompt_tokens)
242
- set_current_context_attribute("output_tokens", output_tokens + completion_tokens)
243
-
276
+
277
+ set_current_context_attribute(
278
+ "input_tokens", input_tokens + prompt_tokens
279
+ )
280
+ set_current_context_attribute(
281
+ "output_tokens", output_tokens + completion_tokens
282
+ )
283
+
244
284
  self.key_pool.decrement_task_count(key)
245
285
  return response # 请求成功,返回结果
246
-
286
+
247
287
  except Exception as e:
248
288
  self.key_pool.decrement_task_count(key)
249
289
  attempt += 1
@@ -255,7 +295,7 @@ class OpenAICompatible(LLM_Interface):
255
295
  )
256
296
 
257
297
  key = self.key_pool.get_least_loaded_key()
258
- self.client = OpenAI(api_key=key, base_url=self.base_url)
298
+ self.client = AsyncOpenAI(api_key=key, base_url=self.base_url)
259
299
 
260
300
  if attempt >= self.max_retries:
261
301
  push_error(
@@ -266,15 +306,20 @@ class OpenAICompatible(LLM_Interface):
266
306
  time.sleep(self.retry_delay) # 重试前等待一段时间
267
307
  return {} # 添加默认返回以满足类型检查,实际上这行代码永远不会执行
268
308
 
269
- def chat_stream(
309
+ async def chat_stream(
270
310
  self,
271
311
  trace_id: str = get_current_trace_id(),
272
312
  stream: Literal[True] = True,
273
- messages: Iterable[Dict[str, str]] = [{"role": "system", "content": "你是一位乐于助人的助手,可以帮助用户解决各种问题。"}],
313
+ messages: Iterable[Dict[str, str]] = [
314
+ {
315
+ "role": "system",
316
+ "content": "你是一位乐于助人的助手,可以帮助用户解决各种问题。",
317
+ }
318
+ ],
274
319
  timeout: Optional[int] = 30,
275
320
  *args,
276
321
  **kwargs,
277
- ) -> Generator[Dict[Any, Any], None, None]:
322
+ ) -> AsyncGenerator[Dict[Any, Any], None]:
278
323
  """执行流式LLM对话请求
279
324
 
280
325
  Args:
@@ -288,18 +333,18 @@ class OpenAICompatible(LLM_Interface):
288
333
  LLM的响应块
289
334
  """
290
335
  key = self.key_pool.get_least_loaded_key()
291
- self.client = OpenAI(api_key=key, base_url=self.base_url)
336
+ self.client = AsyncOpenAI(api_key=key, base_url=self.base_url)
292
337
 
293
338
  attempt = 0
294
339
  while attempt < self.max_retries:
295
340
  try:
296
341
  self.key_pool.increment_task_count(key)
297
342
  data = json.dumps(messages, ensure_ascii=False, indent=4)
298
- app_log(
343
+ push_debug(
299
344
  f"OpenAICompatible::chat_stream: {self.model_name} request with API key: {key}, and message: {data}",
300
- location=get_location()
345
+ location=get_location(),
301
346
  )
302
- response: Generator[Dict[Any, Any], None, None] = self.client.chat.completions.create( # type: ignore
347
+ response = await self.client.chat.completions.create( # type: ignore
303
348
  messages=messages, # type: ignore
304
349
  model=self.model_name,
305
350
  stream=stream,
@@ -311,7 +356,7 @@ class OpenAICompatible(LLM_Interface):
311
356
  total_prompt_tokens = 0
312
357
  total_completion_tokens = 0
313
358
 
314
- for chunk in response:
359
+ async for chunk in response:
315
360
  yield chunk # 按块返回生成器中的数据
316
361
  if chunk.choices and chunk.choices[0].delta: # type: ignore
317
362
  if not chunk.choices[0].delta.tool_calls: # type: ignore
@@ -323,8 +368,12 @@ class OpenAICompatible(LLM_Interface):
323
368
  input_tokens = get_current_context_attribute("input_tokens") or 0
324
369
  output_tokens = get_current_context_attribute("output_tokens") or 0
325
370
 
326
- set_current_context_attribute("input_tokens", input_tokens + total_prompt_tokens)
327
- set_current_context_attribute("output_tokens", output_tokens + total_completion_tokens)
371
+ set_current_context_attribute(
372
+ "input_tokens", input_tokens + total_prompt_tokens
373
+ )
374
+ set_current_context_attribute(
375
+ "output_tokens", output_tokens + total_completion_tokens
376
+ )
328
377
 
329
378
  self.key_pool.decrement_task_count(key)
330
379
  break # 如果成功,跳出重试循环
@@ -335,20 +384,21 @@ class OpenAICompatible(LLM_Interface):
335
384
  data = json.dumps(messages, ensure_ascii=False, indent=4)
336
385
  push_warning(
337
386
  f"{self.model_name} Interface attempt {attempt} failed: With message : {data} send, \n but exception : {str(e)} was caught",
338
- location=get_location()
387
+ location=get_location(),
339
388
  )
340
389
 
341
390
  key = self.key_pool.get_least_loaded_key()
342
- self.client = OpenAI(api_key=key, base_url=self.base_url)
391
+ self.client = AsyncOpenAI(api_key=key, base_url=self.base_url)
343
392
 
344
393
  if attempt >= self.max_retries:
345
394
  push_error(
346
395
  f"Max retries reached. {self.model_name} Failed to get a response for {data}",
347
- location=get_location()
396
+ location=get_location(),
348
397
  )
349
398
  raise e
350
399
  time.sleep(self.retry_delay)
351
-
400
+
352
401
  # 下面是一个空生成器,用于满足类型检查,实际上永远不会执行到这里
353
402
  if False:
354
- yield {}
403
+ yield {}
404
+
@@ -1,6 +1,8 @@
1
- from SimpleLLMFunc.llm_decorator.llm_function_decorator import llm_function
2
- from SimpleLLMFunc.llm_decorator.llm_chat_decorator import llm_chat
1
+ from SimpleLLMFunc.llm_decorator.llm_function_decorator import llm_function, async_llm_function
2
+ from SimpleLLMFunc.llm_decorator.llm_chat_decorator import llm_chat, async_llm_chat
3
3
  __all__ = [
4
4
  "llm_function",
5
- "llm_chat"
5
+ "async_llm_function",
6
+ "llm_chat",
7
+ "async_llm_chat"
6
8
  ]