Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 4 additions & 21 deletions .env.example
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions anp_examples/anp_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
}
]
64 changes: 28 additions & 36 deletions anp_examples/simple_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
108 changes: 108 additions & 0 deletions anp_examples/utils/json_utils.py
Original file line number Diff line number Diff line change
@@ -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)
25 changes: 6 additions & 19 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)}")
raise ValueError(f"Missing required environment variables for {', '.join(missing_vars)}")
Loading