diff --git a/.env.example b/.env.example index e086367..3abd47f 100644 --- a/.env.example +++ b/.env.example @@ -1,23 +1,6 @@ -# Azure OpenAI Configuration -# Copy this file to .env and fill in your actual values. +# Replace [YOUR_API_KEY] with your actual API key. +# You can check whether your model is supported and view parameter descriptions in the `./freshguide.md` file. -# AZURE_OPENAI_API_KEY=YOUR_AZURE_OPENAI_API_KEY -# AZURE_OPENAI_ENDPOINT=YOUR_AZURE_OPENAI_ENDPOINT -# AZURE_OPENAI_API_VERSION=2024-02-01 -# # gpt-4o or gpt4o-mini -# AZURE_OPENAI_DEPLOYMENT=gpt-4o -# # gpt-4o or gpt-4o-mini -# AZURE_OPENAI_MODEL_NAME=gpt-4o - - - -MODEL_PROVIDER=openai # 可选值: dashscope / openai - -DASHSCOPE_API_KEY = [YOUR_AZURE_OPENAI_API_KEY] -DASHSCOPE_BASE_URL = https://dashscope.aliyuncs.com/compatible-mode/v1 -DASHSCOPE_MODEL_NAME = qwen2.5-14b-instruct - - -OPENAI_API_KEY = [YOUR_COMPOSABLE_OPENAI_API_KEY] -OPENAI_BASE_URL = https://api.360.cn/v1 +OPENAI_API_KEY = YOUR_API_KEY +OPENAI_BASE_URL = https://api.openai.com/v1 OPENAI_MODEL = gpt-4o \ No newline at end of file diff --git a/.gitignore b/.gitignore index ef3043a..b09f95b 100644 --- a/.gitignore +++ b/.gitignore @@ -165,7 +165,7 @@ cython_debug/ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +.idea/ # PyPI configuration file .pypirc diff --git a/anp_examples/anp_tool.py b/anp_examples/anp_tool.py index 9dafcbe..03dea23 100644 --- a/anp_examples/anp_tool.py +++ b/anp_examples/anp_tool.py @@ -244,3 +244,17 @@ async def _process_response(self, response, url): result["url"] = str(url) return result + + @property + def tools(self): + """Get the list of available tools""" + return [ + { + "type": "function", + "function": { + "name": "anp_tool", + "description": self.description, + "parameters": self.parameters, + }, + } + ] \ No newline at end of file diff --git a/anp_examples/simple_example.py b/anp_examples/simple_example.py index 5c03f90..8acf08f 100644 --- a/anp_examples/simple_example.py +++ b/anp_examples/simple_example.py @@ -6,11 +6,12 @@ from pathlib import Path from openai import AsyncAzureOpenAI from dotenv import load_dotenv + +from anp_examples.utils.json_utils import robust_json_parse from anp_examples.utils.log_base import set_log_color_level from anp_examples.anp_tool import ANPTool # Import ANPTool -from openai import AsyncOpenAI,OpenAI -from config import validate_config, DASHSCOPE_API_KEY, DASHSCOPE_BASE_URL, DASHSCOPE_MODEL_NAME, OPENAI_API_KEY, \ - OPENAI_BASE_URL +from openai import AsyncOpenAI, OpenAI +from config import validate_config, OPENAI_API_KEY, OPENAI_BASE_URL, OPENAI_MODEL # Get the absolute path to the root directory ROOT_DIR = Path(__file__).resolve().parent.parent @@ -91,15 +92,15 @@ def get_available_tools(anp_tool_instance): async def handle_tool_call( - tool_call: Any, - messages: List[Dict], - anp_tool: ANPTool, - crawled_documents: List[Dict], - visited_urls: set, + tool_call: Any, + messages: List[Dict], + anp_tool: ANPTool, + crawled_documents: List[Dict], + visited_urls: set, ) -> None: """Handle tool call""" function_name = tool_call.function.name - function_args = json.loads(tool_call.function.arguments) + function_args = robust_json_parse(tool_call.function.arguments) if function_name == "anp_tool": url = function_args.get("url") @@ -144,12 +145,12 @@ async def handle_tool_call( async def simple_crawl( - user_input: str, - task_type: str = "general", - did_document_path: Optional[str] = None, - private_key_path: Optional[str] = None, - max_documents: int = 10, - initial_url: str = "https://agent-search.ai/ad.json", + user_input: str, + task_type: str = "general", + did_document_path: Optional[str] = None, + private_key_path: Optional[str] = None, + max_documents: int = 10, + initial_url: str = "https://agent-search.ai/ad.json", ) -> Dict[str, Any]: """ Simplified crawling logic: let the model decide the crawling path autonomously @@ -182,21 +183,11 @@ async def simple_crawl( # azure_deployment=os.getenv("AZURE_OPENAI_DEPLOYMENT"), # ) - # 根据 MODEL_PROVIDER 环境变量选择不同的客户端 - model_provider = os.getenv("MODEL_PROVIDER", "dashscope").lower() - - if model_provider == "dashscope": - client = AsyncOpenAI( - api_key=DASHSCOPE_API_KEY, - base_url=DASHSCOPE_BASE_URL - ) - elif model_provider == "openai": - client = AsyncOpenAI( - api_key=OPENAI_API_KEY, - base_url=OPENAI_BASE_URL - ) - else: - raise ValueError(f"Unsupported MODEL_PROVIDER: {model_provider}") + # Initialize Async OpenAI client + client = AsyncOpenAI( + api_key=OPENAI_API_KEY, + base_url=OPENAI_BASE_URL + ) # Get initial URL content try: @@ -252,10 +243,11 @@ async def simple_crawl( # Get model response completion = await client.chat.completions.create( - model = DASHSCOPE_MODEL_NAME, - messages = messages, - tools = get_available_tools(anp_tool), - tool_choice = "auto", + model=OPENAI_MODEL, + messages=messages, + tools=anp_tool.tools, + tool_choice="auto", + temperature=0.0 ) response_message = completion.choices[0].message @@ -288,8 +280,8 @@ async def simple_crawl( # If the maximum number of documents to crawl is reached, make a final summary if ( - len(crawled_documents) >= max_documents - and current_iteration < max_documents + len(crawled_documents) >= max_documents + and current_iteration < max_documents ): logging.info( f"Reached the maximum number of documents to crawl {max_documents}, making final summary" diff --git a/anp_examples/utils/json_utils.py b/anp_examples/utils/json_utils.py new file mode 100644 index 0000000..4cbace5 --- /dev/null +++ b/anp_examples/utils/json_utils.py @@ -0,0 +1,108 @@ +# AgentConnect: https://github.com/agent-network-protocol/AgentConnect +# Author: Yu Chen +# Website: https://agent-network-protocol.com/ +# +# This project is open-sourced under the MIT License. For details, please see the LICENSE file. + +import json +from typing import Any + + +def robust_json_parse(raw_args: str) -> Any: + """ + Ultra-robust JSON parser that handles: + - Standard JSON + - Quote-wrapped JSON + - Missing braces/quotes + - Partially wrapped strings + - Any combination of the above + + Args: + raw_args: Input string that may contain malformed JSON + + Returns: + Parsed Python object + + Raises: + ValueError: When JSON cannot be repaired + """ + + json_data = _json_parse(raw_args) + if isinstance(json_data, str): + # Try to parse again after removing any trailing comma + json_data = _json_parse(json_data) + + return json_data + +def _json_parse(raw_args: str) -> Any: + """ + Parses JSON string robustly, handling common errors such as missing braces/quotes, + """ + if not isinstance(raw_args, str): + raw_args = str(raw_args).strip() + + raw_args = raw_args.replace('\\"', '"') # Handle escaped quotes + raw_args = raw_args.replace('\"', '"') # Handle escaped quotes + + # Case 1: Try direct parse first (for valid JSON) + try: + return json.loads(raw_args) + except json.JSONDecodeError: + pass + + # Case 2: Handle quote-wrapped or partially wrapped cases + modified = raw_args + was_quoted = False + + # Remove outer quotes if they exist (complete or partial) + if modified.startswith('"'): + was_quoted = True + modified = modified[1:] + if modified.endswith('"'): + modified = modified[:-1] + + # Case 2a: Try parsing after removing outer quotes + try: + return json.loads(modified) + except json.JSONDecodeError as e: + # Case 2b: Try repairing structure + try: + return _repair_and_parse(modified, was_quoted) + except ValueError: + # Case 3: Final attempt with original string + try: + return _repair_and_parse(raw_args, False) + except ValueError as e: + raise ValueError(f"Failed to parse JSON: {raw_args}") from e + + +def _repair_and_parse(json_str: str, was_quoted: bool) -> Any: + """Handles JSON repair and final parsing attempt""" + # Balance braces + if json_str.count('{') > json_str.count('}'): + json_str = json_str.rstrip() + '}' + + # Balance quotes if we removed outer quotes + if was_quoted and json_str.count('"') % 2 != 0: + json_str += '"' + + # Remove any trailing comma + json_str = json_str.rstrip().rstrip(',') + + try: + return json.loads(json_str) + except json.JSONDecodeError as e: + raise ValueError("Repair failed") from e + + +if __name__ == '__main__': + res = robust_json_parse('{"key": "value"}') # Standard + print(res) + res = robust_json_parse('"{\\"key\\": \\"value\\"}"') # Wrapped + print(res) + res = robust_json_parse('{"key": "value"') # Missing } + print(res) + res = robust_json_parse('"{"key": "value"') # Partial wrap + print(res) + res = robust_json_parse('"{') # Extreme case + print(res) \ No newline at end of file diff --git a/config.py b/config.py index 916e443..f334563 100644 --- a/config.py +++ b/config.py @@ -11,35 +11,22 @@ from dotenv import load_dotenv # Get the project root directory (assuming tests folder is directly under root) -#ROOT_DIR = Path(__file__).parent.parent.parent +# ROOT_DIR = Path(__file__).parent.parent.parent ROOT_DIR = Path(__file__).parent # Load environment variables from root .env file load_dotenv(ROOT_DIR / '.env') -# OpenRouter - DeepSeek API configurations -DASHSCOPE_API_KEY = os.getenv('DASHSCOPE_API_KEY') -DASHSCOPE_BASE_URL = os.getenv('DASHSCOPE_BASE_URL') -DASHSCOPE_MODEL_NAME = os.getenv('DASHSCOPE_MODEL_NAME') - -model_provider = os.getenv("MODEL_PROVIDER", "openai").lower() - # OpenAi -OPENAI_API_KEY=os.getenv('OPENAI_API_KEY') -OPENAI_BASE_URL=os.getenv('OPENAI_BASE_URL') -OPENAI_MODEL=os.getenv('OPENAI_MODEL') - +OPENAI_API_KEY = os.getenv('OPENAI_API_KEY') +OPENAI_BASE_URL = os.getenv('OPENAI_BASE_URL') +OPENAI_MODEL = os.getenv('OPENAI_MODEL') def validate_config(): """Validate that at least one set of required environment variables is set""" - if model_provider == "dashscope": - required_vars = ["DASHSCOPE_API_KEY", "DASHSCOPE_BASE_URL", "DASHSCOPE_MODEL_NAME"] - elif model_provider == "openai": - required_vars = ["OPENAI_API_KEY", "OPENAI_BASE_URL"] - else: - raise ValueError(f"Unsupported MODEL_PROVIDER: {model_provider}") + required_vars = ["OPENAI_API_KEY", "OPENAI_BASE_URL", "OPENAI_MODEL"] missing_vars = [var for var in required_vars if not os.getenv(var)] if missing_vars: - raise ValueError(f"Missing required environment variables for {model_provider}: {', '.join(missing_vars)}") \ No newline at end of file + raise ValueError(f"Missing required environment variables for {', '.join(missing_vars)}") diff --git a/freshguide.md b/freshguide.md new file mode 100644 index 0000000..171af6c --- /dev/null +++ b/freshguide.md @@ -0,0 +1,103 @@ +# 配置.env文件 +本项目需要您的模型API是否兼容OpenAI API的格式,您可以查看下方表格,确认您的模型供应商(或者模型推理框架服务)是否支持。 +> 注:本项目需要您的模型支持function call的功能(在未来,我们将适配不支持function call的模型,以供兼容更多的模型) + +以下是截至2025年5月主流模型供应商及推理引擎的完整汇总表格,包含名称、API地址、文档地址及OpenAI API兼容性标记: + +### **一、模型供应商(兼容OpenAI API格式)** +| 供应商名称 | API地址 | API文档地址 | 支持OpenAI格式 | +|------------------|---------------------------------------------|---------------------------------------------|----------------| +| **智谱清言** | `https://open.bigmodel.cn/api/paas/v4` | [文档](https://bigmodel.cn/dev/api/thirdparty-frame/openai-sdk) | ✅ | +| **科大讯飞星火** | `https://spark-api-open.xf-yun.com/v1` | [文档](https://www.xfyun.cn/doc/spark/HTTP调用文档.html#_7-使用openai-sdk请求示例) | ✅ | +| **百川大模型** | `https://api.baichuan-ai.com/v1` | [文档](https://platform.baichuan-ai.com/docs/api#python-client) | ✅ | +| **豆包大模型** | `https://ark.cn-beijing.volces.com/api/v3` | [文档](https://www.volcengine.com/docs/82379/1330626) | ✅ | +| **月之暗面** | `https://api.moonshot.cn/v1` | [文档](https://platform.moonshot.cn/docs/guide/migrating-from-openai-to-kimi#关于-api-兼容性) | ✅ | +| **通义千问** | `https://dashscope.aliyuncs.com/compatible-mode/v1` | [文档](https://help.aliyun.com/zh/model-studio/getting-started/first-api-call-to-qwen) | ✅ | +| **腾讯混元** | `https://api.hunyuan.cloud.tencent.com/v1` | [文档](https://cloud.tencent.com/document/product/1729/111007) | ✅ | +| **商汤日日新** | `https://api.sensenova.cn/compatible-mode/v1` | [文档](https://www.sensecore.cn/help/docs/model-as-a-service/nova/overview/compatible-mode) | ✅ | +| **DeepSeek** | `https://api.deepseek.com/v1` | [文档](https://platform.deepseek.com/api-docs) | ✅ | +| **硅基流动** | `https://api.siliconflow.cn/v1` | [文档](https://cloud.siliconflow.cn/docs) | ✅ | +| **360智脑** | 需企业申请(未公开统一API) | [测评报告](https://www.cluebenchmarks.com) | ❌ | +| **OpenAI** | `https://api.openai.com/v1` | [官方文档](https://platform.openai.com/docs) | ✅ | +| **Anthropic** | `https://api.anthropic.com/v1` | [文档](https://docs.anthropic.com) | ⚠️(部分兼容) | + +### **二、开源推理引擎(兼容OpenAI API)** +| 引擎名称 | API地址(本地部署) | 文档地址 | 支持OpenAI格式 | +|----------------|--------------------------|-------------------------------------------|----------------| +| **vLLM** | `http://localhost:8000/v1` | [GitHub](https://github.com/vllm-project/vllm) | ✅ | +| **SGLang** | `http://localhost:3000/v1` | [GitHub](https://github.com/sgl-project/sglang) | ✅ | +| **Ollama** | `http://localhost:11434/v1` | [官网](https://ollama.ai) | ✅ | +| **LMDeploy** | `http://localhost:23333/v1` | [文档](https://lmdeploy.readthedocs.io) | ✅ | +| **TensorRT-LLM** | `http://localhost:8000/v1` | [NVIDIA文档](https://github.com/NVIDIA/TensorRT-LLM) | ✅ | +| **Mooncake** | 需定制部署 | [GitHub](https://github.com/mooncake-project) | ❌ | + +### **关键说明** +1. **兼容性标记**: + - ✅:完全兼容OpenAI API格式。 + - ⚠️:需调整部分参数或接口。 + - ❌:不兼容或需深度适配。 +2. **国内服务**:如360智脑、百度等可能需企业认证或创建应用。 +3. **自托管引擎**:vLLM、Ollama等需自行部署,硬件要求较高。 + + +### **配置文件注意事项** +- OPENAI_BASE_URL:请填写您所使用的模型供应商的API地址。 + - 这里的URL并非完整的API地址,而是base_url, 其完整路径为: `https:///chat/completions` + - 假设您的API地址为:`https://api.deepseek.com/v1/chat/completions`, 则填写`https://api.deepseek.com/v1` 即可 + - 因此,判断模型API是否兼容OpenAI API格式,可以简单的判断API地址的格式是否是 `http://xxx/v1/chat/completions` + - 也有的模型供应商的API地址并不包含`v1` 等版本号,此时您可以尝试将`v1` 等版本号去掉,比如硅基流动,直接填写`https://api.siliconflow.cn` 即可。 +- OPENAI_API_KEY:请填写您的OpenAI API Key。 + - **如果您是使用(vLLM、Ollama等)私有化部署的模型**,您可能没有手动设置API Key,此时这里您可以**直接填`sk-api-key`**。 + - 私有化部署可能没有API Key,但您此处不得为空,所以随意填写一个字符串,确保不为空即可。 +- OPENAI_MODEL:请填写您所使用的模型的ID。 + + +# Configuring the .env File +This project requires your model API to be compatible with the OpenAI interface format. Please refer to the table below to confirm whether your model provider (model inference framework service) supports it. +> Note: This project requires your model to support function call capabilities (in the future, we will adapt to models that do not support function calls to ensure compatibility with more models). + +Below is a comprehensive table of mainstream model providers and inference engines as of May 2025, including names, API addresses, documentation links, and OpenAI API compatibility markers: + +### **I. Model Providers (Compatible with OpenAI API Format)** +| Provider Name | API Address | Documentation Link | OpenAI Format Support | +|---------------------|-------------------------------------------------|-----------------------------------------------------|-----------------------| +| **Zhipu Qingyan** | `https://open.bigmodel.cn/api/paas/v4` | [Docs](https://bigmodel.cn/dev/api/thirdparty-frame/openai-sdk) | ✅ | +| **iFlytek Spark** | `https://spark-api-open.xf-yun.com/v1` | [Docs](https://www.xfyun.cn/doc/spark/HTTP调用文档.html#_7-使用openai-sdk请求示例) | ✅ | +| **Baichuan** | `https://api.baichuan-ai.com/v1` | [Docs](https://platform.baichuan-ai.com/docs/api#python-client) | ✅ | +| **Doubao** | `https://ark.cn-beijing.volces.com/api/v3` | [Docs](https://www.volcengine.com/docs/82379/1330626) | ✅ | +| **Moonshot AI** | `https://api.moonshot.cn/v1` | [Docs](https://platform.moonshot.cn/docs/guide/migrating-from-openai-to-kimi#关于-api-兼容性) | ✅ | +| **Tongyi Qianwen** | `https://dashscope.aliyuncs.com/compatible-mode/v1` | [Docs](https://help.aliyun.com/zh/model-studio/getting-started/first-api-call-to-qwen) | ✅ | +| **Tencent Hunyuan** | `https://api.hunyuan.cloud.tencent.com/v1` | [Docs](https://cloud.tencent.com/document/product/1729/111007) | ✅ | +| **SenseTime Nova** | `https://api.sensenova.cn/compatible-mode/v1` | [Docs](https://www.sensecore.cn/help/docs/model-as-a-service/nova/overview/compatible-mode) | ✅ | +| **DeepSeek** | `https://api.deepseek.com/v1` | [Docs](https://platform.deepseek.com/api-docs) | ✅ | +| **SiliconFlow** | `https://api.siliconflow.cn/v1` | [Docs](https://cloud.siliconflow.cn/docs) | ✅ | +| **360 AI Brain** | Enterprise application required (no public API) | [Evaluation Report](https://www.cluebenchmarks.com) | ❌ | +| **OpenAI** | `https://api.openai.com/v1` | [Official Docs](https://platform.openai.com/docs) | ✅ | +| **Anthropic** | `https://api.anthropic.com/v1` | [Docs](https://docs.anthropic.com) | ⚠️ (Partial compatibility) | + +### **II. Open-Source Inference Engines (Compatible with OpenAI API)** +| Engine Name | Local API Address | Documentation Link | OpenAI Format Support | +|-----------------|---------------------------|---------------------------------------------|-----------------------| +| **vLLM** | `http://localhost:8000/v1` | [GitHub](https://github.com/vllm-project/vllm) | ✅ | +| **SGLang** | `http://localhost:3000/v1` | [GitHub](https://github.com/sgl-project/sglang) | ✅ | +| **Ollama** | `http://localhost:11434/v1` | [Website](https://ollama.ai) | ✅ | +| **LMDeploy** | `http://localhost:23333/v1` | [Docs](https://lmdeploy.readthedocs.io) | ✅ | +| **TensorRT-LLM** | `http://localhost:8000/v1` | [NVIDIA Docs](https://github.com/NVIDIA/TensorRT-LLM) | ✅ | +| **Mooncake** | Custom deployment required | [GitHub](https://github.com/mooncake-project) | ❌ | + +### **Key Notes** +1. **Compatibility Markers**: + - ✅: Fully compatible with OpenAI API format. + - ⚠️: Requires partial parameter or interface adjustments. + - ❌: Not compatible or requires deep adaptation. +2. **Domestic Services**: Providers like 360 AI Brain or Baidu may require enterprise authentication or application creation. +3. **Self-Hosted Engines**: vLLM, Ollama, etc., require self-deployment and have high hardware requirements. + +### **Configuration File Notes** +- **OPENAI_BASE_URL**: Enter the API address of your chosen model provider. + - This URL is not the full API address but the base URL. The full path is: `https:///chat/completions`. + - For example, if your API address is `https://api.deepseek.com/v1/chat/completions`, simply enter `https://api.deepseek.com/v1`. +- **OPENAI_API_KEY**: Enter your OpenAI API key. + - **If you are using a privately deployed model (e.g., vLLM, Ollama)**, you may not have manually set an API key. In this case, you can **enter `sk-api-key` directly**. + - Privately deployed models may not require an API key, but this field cannot be empty. You can enter any string to ensure it is not blank. +- **OPENAI_MODEL**: Enter the ID of the model you are using.