LightAgent 0.3.1__tar.gz → 0.3.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.
@@ -3,33 +3,32 @@
3
3
 
4
4
  """
5
5
  作者: [weego/WXAI-Team]
6
- 版本: 0.3.0
7
- 最后更新: 2025-03-31
6
+ 版本: 0.3.3
7
+ 最后更新: 2025-05-05
8
8
  """
9
9
 
10
- import re
11
- from typing import List, Dict, Any, Callable, Union, Iterable, Optional, Generator, AsyncGenerator
12
- from copy import deepcopy
10
+ import asyncio
11
+ import importlib
13
12
  import importlib.util
14
- import random
13
+ import inspect
15
14
  import json
16
- from datetime import datetime
17
- from openai import OpenAI
18
15
  import logging
19
16
  import os
20
- import httpx
21
- import importlib
22
- from openai.types.chat import ChatCompletionChunk
23
- import inspect
17
+ import random
18
+ import re
24
19
  import traceback
25
- from mcp import ClientSession, StdioServerParameters, types
26
20
  from contextlib import AsyncExitStack
21
+ from copy import deepcopy
22
+ from datetime import datetime
23
+ from functools import partial
24
+ from typing import List, Dict, Any, Callable, Union, Optional, Generator, AsyncGenerator
25
+ from uuid import uuid4
27
26
 
27
+ import httpx
28
+ from mcp import ClientSession, StdioServerParameters
28
29
  from mcp.client.sse import sse_client
29
30
  from mcp.client.stdio import stdio_client
30
- import asyncio
31
- from functools import partial
32
-
31
+ from openai.types.chat import ChatCompletionChunk
33
32
 
34
33
  # 全局工具注册表
35
34
  _FUNCTION_MAPPINGS = {} # 工具名称 -> 工具函数
@@ -37,7 +36,10 @@ _FUNCTION_INFO = {} # 工具名称 -> 工具info信息
37
36
  _OPENAI_FUNCTION_SCHEMAS = [] # OpenAI 格式的工具描述
38
37
  _PROMPT_FUNCTION_SCHEMAS = [] # prompt 格式的工具描述
39
38
 
40
- __version__ = "0.3.0" # 你可以根据需要设置版本号
39
+ __version__ = "0.3.3" # 你可以根据需要设置版本号
40
+
41
+
42
+ # openai.langfuse_auth_check()
41
43
 
42
44
 
43
45
  def register_tool_manually(tools: List[Union[str, Callable]]) -> bool:
@@ -181,6 +183,44 @@ def get_tools_str() -> str:
181
183
  return tools_str
182
184
 
183
185
 
186
+ def filter_tools_schemas(refined_content: str) -> json:
187
+ """
188
+ 根据refined_content中的工具列表过滤全局_OPENAI_FUNCTION_SCHEMAS
189
+ :param refined_content: 包含工具列表的JSON字符串
190
+ """
191
+ # global _OPENAI_FUNCTION_SCHEMAS # 声明操作全局变量
192
+ """安全解析可能包含 Markdown 代码块的 JSON"""
193
+ refined_content = refined_content.strip()
194
+ if refined_content.startswith('```json') and refined_content.endswith('```'):
195
+ refined_content = refined_content[7:-3].strip() # 去除 ```json 和 ```
196
+ try:
197
+ # 解析工具列表
198
+ parsed_data: Dict[str, List[Dict]] = json.loads(refined_content)
199
+ valid_tools = {tool["name"].strip().lower() for tool in parsed_data.get("tools", [])}
200
+
201
+ # 原地过滤操作
202
+ filtered_schemas: List[Dict] = []
203
+ for schema in _OPENAI_FUNCTION_SCHEMAS:
204
+ if not isinstance(schema, dict):
205
+ continue
206
+
207
+ # 深度检查结构
208
+ function_info = schema.get("function", {})
209
+ if isinstance(function_info, dict):
210
+ schema_name = function_info.get("name", "").strip().lower()
211
+ if schema_name in valid_tools:
212
+ filtered_schemas.append(schema)
213
+
214
+ # 直接替换全局变量内容
215
+ # _OPENAI_FUNCTION_SCHEMAS[:] = filtered_schemas
216
+ return filtered_schemas
217
+
218
+ except (json.JSONDecodeError, KeyError, AttributeError) as e:
219
+ # 错误处理:清空工具列表并记录日志
220
+ # _OPENAI_FUNCTION_SCHEMAS.clear()
221
+ raise ValueError(f"工具过滤失败: {str(e)}") from e
222
+
223
+
184
224
  class MCPClientManager:
185
225
  """增强版MCP客户端管理器"""
186
226
 
@@ -379,7 +419,7 @@ class MCPClientManager:
379
419
 
380
420
 
381
421
  class LightAgent:
382
- __version__ = "0.3.0" # 将版本号放在类中
422
+ __version__ = "0.3.3" # 将版本号放在类中
383
423
 
384
424
  def __init__(
385
425
  self,
@@ -391,16 +431,18 @@ class LightAgent:
391
431
  api_key: str | None = None, # 模型 api key
392
432
  base_url: str | httpx.URL | None = None, # 模型 base url
393
433
  websocket_base_url: str | httpx.URL | None = None, # 模型 websocket base url
394
- memory=None, # 支持外部传入记忆模块
434
+ memory: str | None = None, # 支持外部传入记忆模块
395
435
  tree_of_thought: bool = False, # 是否启用链式思考
396
436
  tot_model: str | None = None, # 链式思考模型
397
437
  tot_api_key: str | None = None, # 链式思考模型API密钥
398
438
  tot_base_url: str | httpx.URL | None = None, # 链式思考模型base_url
439
+ filter_tools: bool = True, # 是否启用工具过滤
399
440
  self_learning: bool = False, # 是否启用agent自我学习
400
441
  tools: List[Union[str, Callable]] = None, # 支持工具混合输入
401
442
  debug: bool = False, # 是否启用调试模式
402
443
  log_level: str = "INFO", # 日志级别(INFO, DEBUG, ERROR)
403
- log_file: Optional[str] = None # 日志文件路径
444
+ log_file: Optional[str] = None, # 日志文件路径
445
+ tracetools: Optional[dict] = None, # log跟踪工具
404
446
  ) -> None:
405
447
  """
406
448
  初始化 LightAgent。
@@ -417,10 +459,12 @@ class LightAgent:
417
459
  :param tot_model: 使用的模型名称。
418
460
  :param tot_api_key: API 密钥。
419
461
  :param tot_base_url: API 的基础 URL。
462
+ :param filter_tools: 是否启用工具过滤。
420
463
  :param tools: 工具列表,支持函数名称(字符串)或函数对象。
421
464
  :param debug: 是否启用调试模式。
422
465
  :param log_level: 日志级别(INFO, DEBUG, ERROR)。
423
466
  :param log_file: 日志文件路径。
467
+ :param tracetools: log跟踪工具。
424
468
  """
425
469
  self.mcp_setting = None
426
470
  self.mcp_client = None
@@ -443,9 +487,11 @@ class LightAgent:
443
487
  self.memory = memory
444
488
  self.tree_of_thought = tree_of_thought
445
489
  self.self_learning = self_learning
490
+ self.filter_tools = filter_tools
446
491
 
447
492
  self.debug = debug
448
493
  self.log_level = log_level.upper()
494
+ self.traceid = "" # 用于存储 traceid
449
495
  # 确保 log 目录存在
450
496
  log_dir = 'logs'
451
497
  if not os.path.exists(log_dir):
@@ -463,7 +509,6 @@ class LightAgent:
463
509
  self.load_tools(tools)
464
510
  # register_tool_manually(tools)
465
511
 
466
-
467
512
  if api_key is None:
468
513
  raise ValueError(
469
514
  "The api_key client option must be set either by passing api_key to the client or by setting the OPENAI_API_KEY environment variable"
@@ -474,10 +519,6 @@ class LightAgent:
474
519
  if base_url is None:
475
520
  base_url = f"https://api.openai.com/v1"
476
521
 
477
- self.client = OpenAI(
478
- base_url=base_url,
479
- api_key=self.api_key
480
- )
481
522
  if self.tree_of_thought:
482
523
  if tot_api_key is None:
483
524
  tot_api_key = api_key
@@ -486,10 +527,36 @@ class LightAgent:
486
527
  if not tot_model:
487
528
  tot_model = "deepseek-r1" # 默认思维推理模型为deepseek-r1
488
529
  self.tot_model = tot_model
489
- self.tot_client = OpenAI(
490
- base_url=tot_base_url,
491
- api_key=tot_api_key
530
+
531
+ if tracetools is None:
532
+ self.tracetools = []
533
+ if tracetools:
534
+ self.tracetools = tracetools
535
+ # 初始化工具列表
536
+ from langfuse.openai import openai as la_openai
537
+ la_openai.langfuse_public_key = self.tracetools['TraceToolConfig']['langfuse_public_key']
538
+ la_openai.langfuse_secret_key = self.tracetools['TraceToolConfig']['langfuse_secret_key']
539
+ la_openai.langfuse_enabled = self.tracetools['TraceToolConfig'][
540
+ 'langfuse_enabled'] # Default is True, set to False to disable Langfuse
541
+ la_openai.langfuse_host = self.tracetools['TraceToolConfig']['langfuse_host'] # 🇪🇺 EU region
542
+ la_openai.base_url = base_url
543
+ la_openai.api_key = self.api_key
544
+ self.client = la_openai
545
+ if self.tree_of_thought:
546
+ la_openai.base_url = tot_base_url
547
+ la_openai.api_key = tot_api_key
548
+ self.tot_client = la_openai
549
+ else:
550
+ from openai import OpenAI as la_openai
551
+ self.client = la_openai(
552
+ base_url=base_url,
553
+ api_key=self.api_key
492
554
  )
555
+ if self.tree_of_thought:
556
+ self.tot_client = la_openai(
557
+ base_url=tot_base_url,
558
+ api_key=tot_api_key
559
+ )
493
560
 
494
561
  def get_tool(self, tool_name: str) -> Callable:
495
562
  """
@@ -596,7 +663,10 @@ class LightAgent:
596
663
  """
597
664
  if not self.debug:
598
665
  return
599
- log_message = f"{action}: {data}"
666
+ if self.traceid is not None:
667
+ log_message = f"[TraceID: {self.traceid}] {action}: {data}"
668
+ else:
669
+ log_message = f"{action}: {data}"
600
670
  if level == "DEBUG":
601
671
  self.logger.debug(log_message)
602
672
  elif level == "INFO":
@@ -605,8 +675,8 @@ class LightAgent:
605
675
  self.logger.error(log_message)
606
676
 
607
677
  async def setup_mcp(
608
- self,
609
- mcp_setting: dict | None = None, # mcp 设置
678
+ self,
679
+ mcp_setting: dict | None = None, # mcp 设置
610
680
  ):
611
681
  if mcp_setting:
612
682
  self.mcp_setting = mcp_setting
@@ -638,6 +708,7 @@ class LightAgent:
638
708
  :param metadata: 元数据。
639
709
  :return: 代理的回复。
640
710
  """
711
+ self.traceid = uuid4().hex
641
712
  self.log("INFO", "run", {"query": query, "user_id": user_id, "light_swarm": light_swarm, "stream": stream})
642
713
  if history is None:
643
714
  history = []
@@ -654,19 +725,25 @@ class LightAgent:
654
725
  # self._transfer_to_agent(light_swarm.agents[target_agent_name], query, stream=stream)
655
726
  # return # 立即结束当前生成器
656
727
 
657
- # 1. 判断是否需要转移任务
728
+ # 0. 判断是否需要转移任务
658
729
  if light_swarm:
659
730
  result = self._handle_task_transfer(query, light_swarm, stream)
660
731
  if result is not None:
661
732
  return result
662
733
 
663
- # 2. 正常处理任务
734
+ # 1. 正常处理任务
664
735
  now = datetime.now()
665
736
  current_date = now.strftime("%Y-%m-%d")
666
737
  current_time = now.strftime("%H:%M:%S")
667
738
  system_prompt = f"##代理名称:{self.name} ##代理指令 /n{self.instructions} ##身份 /n {self.role} /n 请一步一步思考来完成用户的要求。尽可能完成用户的回答,如果有补充信息,请参考补充信息来调用工具,直到获取所有满足用户的提问所需的答案。 /n 今日的日期: {current_date} 当前时间: {current_time}"
668
739
  params = dict(model=self.model, stream=stream)
669
740
  memory = ''
741
+
742
+ # 2.添加langfuse的session
743
+ if self.tracetools:
744
+ params["session_id"] = self.traceid
745
+ self.log("DEBUG", "Query Trace ID", {"query": query})
746
+
670
747
  # 3. 从记忆中检索相关内容&保存记忆
671
748
  if self.memory:
672
749
  related_memories = self.memory.retrieve(query=query, user_id=user_id)
@@ -680,19 +757,29 @@ class LightAgent:
680
757
  query = f"{memory}\n##用户提问:\n{query}"
681
758
  # print(query)
682
759
 
683
- # 4. 拼接tools工具
684
- tools = get_tools()
760
+ # 4. 思维链
761
+ active_tools = []
762
+ if self.tree_of_thought:
763
+ tot_response, active_tools = self.run_thought(query=query)
764
+ system_prompt = system_prompt + f" /n ##以下是问题的补充说明 /n {tot_response}"
765
+ self.log("DEBUG", "tree_of_thought", {"response": tot_response, "active_tools": active_tools})
766
+
767
+ # 5. 拼接tools工具
768
+ # 带类型校验 自适应工具机制
769
+ try:
770
+ tools = active_tools if (
771
+ len(active_tools) > 0
772
+ ) else get_tools()
773
+ except TypeError:
774
+ tools = get_tools()
775
+ # 带类型校验 自适应工具机制
776
+ # tools = get_tools() # v0.2.X的工具选取机制
685
777
  if tools:
686
778
  self.log("DEBUG", "register_tools", {"tools": list(_FUNCTION_MAPPINGS.keys())})
779
+ self.log("DEBUG", "active_tools", {"tools": tools})
687
780
  params["tools"] = tools
688
781
  params["tool_choice"] = "auto"
689
782
 
690
- # 5. 思维链
691
- if self.tree_of_thought:
692
- tot_response = self.run_thought(query=query)
693
- system_prompt = system_prompt + f" /n ##以下是问题的补充说明 /n {tot_response}"
694
- self.log("DEBUG", "tree_of_thought", {"response": tot_response})
695
-
696
783
  # 6. 调用核心运行逻辑
697
784
  params["messages"] = [{"role": "system", "content": system_prompt}]
698
785
  # 将历史对话添加到消息列表中
@@ -700,6 +787,7 @@ class LightAgent:
700
787
  params["messages"].append({"role": item["role"], "content": item["content"]})
701
788
  # 最后添加当前用户的查询信息
702
789
  params["messages"].append({"role": "user", "content": query})
790
+
703
791
  response = self.client.chat.completions.create(**params)
704
792
 
705
793
  result = self._core_run_logic(response, params, stream, max_retry)
@@ -1075,10 +1163,10 @@ class LightAgent:
1075
1163
  :param related_memories: 从记忆中检索到的相关内容。
1076
1164
  :return: 结合记忆后的上下文。
1077
1165
  """
1078
- if not related_memories or not related_memories["memories"]:
1166
+ if not related_memories or not related_memories["results"]:
1079
1167
  return ""
1080
1168
 
1081
- memory_context = "\n".join([m["memory"] for m in related_memories["memories"]])
1169
+ memory_context = "\n".join([m["memory"] for m in related_memories["results"]])
1082
1170
  if not memory_context:
1083
1171
  return ""
1084
1172
 
@@ -1093,10 +1181,10 @@ class LightAgent:
1093
1181
  :param agent_memories: 从记忆中检索到的相关内容。
1094
1182
  :return: 结合记忆后的上下文。
1095
1183
  """
1096
- if not agent_memories or not agent_memories["memories"]:
1184
+ if not agent_memories or not agent_memories["results"]:
1097
1185
  return ""
1098
1186
 
1099
- memory_context = "\n".join([m["memory"] for m in agent_memories["memories"]])
1187
+ memory_context = "\n".join([m["memory"] for m in agent_memories["results"]])
1100
1188
  if not memory_context:
1101
1189
  return ""
1102
1190
 
@@ -1104,7 +1192,7 @@ class LightAgent:
1104
1192
  self.log("DEBUG", "agent_memories", {"memory_context": memory_context})
1105
1193
  return prompt
1106
1194
 
1107
- def run_thought(self, query: str, stream=False, tools=None):
1195
+ def run_thought(self, query: str) -> tuple:
1108
1196
  """使用思维树的方式 让大模型先根据get_tools_str生成一个解答用户query的工具使用计划"""
1109
1197
  tot_model = self.tot_model # self.model
1110
1198
  tools = get_tools_str()
@@ -1113,30 +1201,66 @@ class LightAgent:
1113
1201
  now = datetime.now()
1114
1202
  current_date = now.strftime("%Y-%m-%d")
1115
1203
  current_time = now.strftime("%H:%M:%S")
1116
- system_prompt = f"""你是一个智能助手,请根据用户输入的问题,结合工具使用计划,生成一个思维树,并按照思维树依次调用工具步骤,最终生成一个最终回答。/n 今日的日期: {current_date} 当前时间: {current_time} /n 工具列表: {tools}"""
1204
+ system_prompt = f"""你是一个智能助手,请根据用户输入的问题,结合工具使用计划,生成一个思维树,并按照思维树依次调用工具步骤,最终生成一个最终回答。\n 今日的日期: {current_date} 当前时间: {current_time} \n 工具列表: {tools}"""
1117
1205
  self.log("DEBUG", "run_thought", {"system_prompt": system_prompt})
1118
1206
 
1119
- # 第一次请求,生成初始的工具使用计划
1120
- params = dict(model=tot_model,
1121
- messages=[{"role": "system", "content": system_prompt}, {"role": "user", "content": query}],
1122
- stream=False)
1123
- response = self.tot_client.chat.completions.create(**params)
1124
- initial_content = response.choices[0].message.content
1125
- self.log("DEBUG", "initial_response", {"response": initial_content})
1126
-
1127
- # 第二次请求,请求大模型反思并生成新的工具使用规划
1128
- reflection_prompt = "请反思你的回答,重新给出新的工具使用规划。仅输出新的工具使用规划,不要输出其他分析和回答。"
1129
- reflection_params = dict(model=tot_model, messages=[
1130
- {"role": "user", "content": f"{system_prompt} /n 开始思考问题: {query}"},
1131
- {"role": "assistant", "content": initial_content},
1132
- {"role": "user", "content": reflection_prompt}
1133
- ], stream=False)
1134
- self.log("DEBUG", "reflection_params", {"params": reflection_params})
1135
-
1136
- reflection_response = self.tot_client.chat.completions.create(**reflection_params)
1137
- refined_content = reflection_response.choices[0].message.content
1138
- self.log("DEBUG", "refined_response", {"response": refined_content})
1139
- return refined_content
1207
+ try:
1208
+ # 1. 第一次请求,生成初始的工具使用计划
1209
+ params = dict(model=tot_model,
1210
+ messages=[{"role": "system", "content": system_prompt}, {"role": "user", "content": query}],
1211
+ stream=False)
1212
+ response = self.tot_client.chat.completions.create(**params)
1213
+ initial_content = response.choices[0].message.content
1214
+ self.log("DEBUG", "initial_response", {"response": initial_content})
1215
+
1216
+ # 2. 第二次请求,请求大模型反思并生成新的工具使用规划
1217
+ reflection_prompt = "请反思你的回答,请严格按照<工具列表>中的工具来规划,不可以创造其他新的工具。请输出新的任务规划,不要输出其他分析和回答。"
1218
+ reflection_params = dict(model=tot_model, messages=[
1219
+ {"role": "user", "content": f"{system_prompt} /n 开始思考问题: {query}"},
1220
+ {"role": "assistant", "content": initial_content},
1221
+ {"role": "user", "content": reflection_prompt}
1222
+ ], stream=False)
1223
+ self.log("DEBUG", "reflection_params", {"params": reflection_params})
1224
+ reflection_response = self.tot_client.chat.completions.create(**reflection_params)
1225
+ refined_content = reflection_response.choices[0].message.content
1226
+ self.log("DEBUG", "refined_response", {"response": refined_content})
1227
+
1228
+ # 获取工具的使用集合
1229
+ tool_reflection_prompt = """请严格按以下要求执行:
1230
+ 1. 分析问题需求并规划需要使用的工具
1231
+ 2. 仅输出包含工具名称的JSON格式结果
1232
+ 3. 使用以下结构(示例):
1233
+ {"tools": [{"name": "工具名称1"}, {"name": "工具名称2"}]}
1234
+ 4. 不要包含任何解释性内容"""
1235
+
1236
+ tool_reflection_params = dict(
1237
+ model=tot_model,
1238
+ messages=[
1239
+ {"role": "system", "content": system_prompt},
1240
+ {"role": "user", "content": f"问题分析请求:{query}"},
1241
+ {"role": "assistant", "content": refined_content},
1242
+ {"role": "user", "content": tool_reflection_prompt}
1243
+ ],
1244
+ response_format={"type": "json_object"}, # 强制JSON输出格式
1245
+ stream=False
1246
+ )
1247
+
1248
+ self.log("DEBUG", "tool_reflection_params", {"params": tool_reflection_params})
1249
+ tool_reflection_response = self.tot_client.chat.completions.create(**tool_reflection_params)
1250
+ tool_reflection_result = tool_reflection_response.choices[0].message.content
1251
+ self.log("DEBUG", "tool_reflection_result", {"result": tool_reflection_result})
1252
+
1253
+ # 3.执行自适应工具过滤
1254
+ current_tools = []
1255
+ if self.filter_tools:
1256
+ current_tools = filter_tools_schemas(tool_reflection_result)
1257
+ self.log("DEBUG", "current_tools", {"get_tools": current_tools})
1258
+
1259
+ return refined_content, current_tools
1260
+
1261
+ except Exception as e:
1262
+ self.log("ERROR", "run_thought_failure", {"error": str(e)})
1263
+ raise RuntimeError(f"思维链执行失败: {str(e)}") from e
1140
1264
 
1141
1265
  def _detect_intent(self, query: str, light_swarm=None) -> Optional[Dict]:
1142
1266
  """