mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-04 14:31:13 +02:00
style: fix E501 line-too-long errors in gns3_copilot
Fix all 423 E501 line length violations across 26 files to comply with
PEP 8 88-character line limit.
Changes:
- Split long f-strings across multiple lines
- Break long docstring descriptions and parameter lists
- Split markdown table rows and list examples
- Break long URL construction f-strings
- Split long logger messages and comments
- Add noqa: E501 for SVG strings (cannot be split)
Modified files:
- agent/: context_manager.py, gns3_copilot.py, model_factory.py
- gns3_client/: connector_factory.py, context_helpers.py, custom_gns3fy.py,
gns3_project_info.py, gns3_topology_reader.py
- prompts/: __init__.py, lab_automation_assistant_prompt.py,
prompt_loader.py, teaching_assistant_prompt.py
- tools_v2/: __init__.py, config_tools_nornir.py, display_tools_nornir.py,
gns3_create_link.py, gns3_create_node.py, gns3_get_node_temp.py,
gns3_start_node.py, gns3_update_node_name.py,
vpcs_tools_telnetlib3.py
- utils/: __init__.py, command_filter.py, get_gns3_device_port.py,
gns3_drawing_utils.py, llm_config_helper.py, message_converters.py,
parse_tool_content.py, tool_call_stream.py
All files now pass ruff E501 checks.
Co-Authored-By: Yue Guobin <yueguobin@outlook.com>
This commit is contained in:
1 parent
25f2d1b56b
commit
7c3b832bca
32 files changed
+1967
-762
No files matched your search
@@ -26,7 +26,7 @@
|
||||
|
||||
GNS3-Copilot Public Model Package
|
||||
|
||||
This package provides reusable public models and utilities for GNS3 network automation tasks.
|
||||
This package provides reusable public models and utilities for GNS3 automation tasks.
|
||||
It contains common functionality that can be shared across different tools and modules.
|
||||
|
||||
Main modules:
|
||||
|
||||
@@ -32,7 +32,6 @@ availability.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -45,6 +44,7 @@ def _get_gns3_copilot_root() -> Path:
|
||||
# Go up to the gns3_copilot directory (utils parent)
|
||||
return current_file.parent.parent
|
||||
|
||||
|
||||
# Default forbidden commands (fallback if file not found)
|
||||
DEFAULT_FORBIDDEN_COMMANDS = [
|
||||
"traceroute",
|
||||
@@ -101,7 +101,10 @@ def _load_forbidden_commands() -> list[str]:
|
||||
forbidden_commands.append(line.lower())
|
||||
|
||||
if not forbidden_commands:
|
||||
logger.warning("No forbidden commands found in %s. Using default list.", file_path)
|
||||
logger.warning(
|
||||
"No forbidden commands found in %s. Using default list.",
|
||||
file_path,
|
||||
)
|
||||
_forbidden_commands_cache = DEFAULT_FORBIDDEN_COMMANDS.copy()
|
||||
else:
|
||||
logger.info(
|
||||
@@ -135,7 +138,9 @@ def reload_forbidden_commands() -> None:
|
||||
"""
|
||||
global _forbidden_commands_cache
|
||||
_forbidden_commands_cache = None
|
||||
logger.info("Forbidden commands cache cleared. Will reload on next access.")
|
||||
logger.info(
|
||||
"Forbidden commands cache cleared. Will reload on next access."
|
||||
)
|
||||
|
||||
|
||||
def get_forbidden_commands() -> list[str]:
|
||||
|
||||
@@ -57,7 +57,9 @@ def get_device_ports_from_topology(
|
||||
Devices that don't exist or missing console_port will not be included
|
||||
"""
|
||||
# Log received parameters
|
||||
logger.info("Called with device_names=%s, project_id=%s", device_names, project_id)
|
||||
logger.info(
|
||||
"Called with device_names=%s, project_id=%s", device_names, project_id
|
||||
)
|
||||
|
||||
try:
|
||||
# Lazy import to avoid circular dependency
|
||||
@@ -77,7 +79,9 @@ def get_device_ports_from_topology(
|
||||
for device_name in device_names:
|
||||
# Check if device exists in topology
|
||||
if device_name not in topology.get("nodes", {}):
|
||||
logger.warning("Device '%s' not found in topology", device_name)
|
||||
logger.warning(
|
||||
"Device '%s' not found in topology", device_name
|
||||
)
|
||||
continue
|
||||
|
||||
node_info = topology["nodes"][device_name]
|
||||
@@ -99,15 +103,30 @@ def get_device_ports_from_topology(
|
||||
# Use defaults if not found in tags
|
||||
if device_type is None:
|
||||
device_type = "cisco_ios_telnet"
|
||||
logger.debug("Device '%s': device_type not found in tags, using default: cisco_ios_telnet", device_name)
|
||||
logger.debug(
|
||||
"Device '%s': device_type not found in tags, using default: "
|
||||
"cisco_ios_telnet",
|
||||
device_name,
|
||||
)
|
||||
else:
|
||||
logger.debug("Device '%s': extracted device_type=%s from tags", device_name, device_type)
|
||||
logger.debug(
|
||||
"Device '%s': extracted device_type=%s from tags",
|
||||
device_name,
|
||||
device_type,
|
||||
)
|
||||
|
||||
if platform is None:
|
||||
platform = "cisco_ios"
|
||||
logger.debug("Device '%s': platform not found in tags, using default: cisco_ios", device_name)
|
||||
logger.debug(
|
||||
"Device '%s': platform not found in tags, using default: cisco_ios",
|
||||
device_name,
|
||||
)
|
||||
else:
|
||||
logger.debug("Device '%s': extracted platform=%s from tags", device_name, platform)
|
||||
logger.debug(
|
||||
"Device '%s': extracted platform=%s from tags",
|
||||
device_name,
|
||||
platform,
|
||||
)
|
||||
|
||||
# Add device to hosts_data
|
||||
hosts_data[device_name] = {
|
||||
|
||||
@@ -135,9 +135,14 @@ def calculate_two_node_shape(
|
||||
node2_center_x = node2["x"] + (node2_width / 2)
|
||||
node2_center_y = node2["y"] + (node2_height / 2)
|
||||
|
||||
distance = math.sqrt((node2_center_x - node1_center_x) ** 2 + (node2_center_y - node1_center_y) ** 2)
|
||||
distance = math.sqrt(
|
||||
(node2_center_x - node1_center_x) ** 2
|
||||
+ (node2_center_y - node1_center_y) ** 2
|
||||
)
|
||||
|
||||
angle_rad = math.atan2(node2_center_y - node1_center_y, node2_center_x - node1_center_x)
|
||||
angle_rad = math.atan2(
|
||||
node2_center_y - node1_center_y, node2_center_x - node1_center_x
|
||||
)
|
||||
angle_deg = round(math.degrees(angle_rad))
|
||||
angle_rad = math.radians(angle_deg)
|
||||
|
||||
@@ -157,10 +162,16 @@ def calculate_two_node_shape(
|
||||
shape_width = rx * 2
|
||||
shape_height = ry * 2
|
||||
|
||||
svg_x = center_x - (rx * math.cos(angle_rad) - ry * math.sin(angle_rad))
|
||||
svg_y = center_y - (rx * math.sin(angle_rad) + ry * math.cos(angle_rad))
|
||||
svg_x = center_x - (
|
||||
rx * math.cos(angle_rad) - ry * math.sin(angle_rad)
|
||||
)
|
||||
svg_y = center_y - (
|
||||
rx * math.sin(angle_rad) + ry * math.cos(angle_rad)
|
||||
)
|
||||
|
||||
shape_svg = generate_ellipse_svg(int(rx), int(ry), color_scheme, int(shape_width), int(shape_height))
|
||||
shape_svg = generate_ellipse_svg(
|
||||
int(rx), int(ry), color_scheme, int(shape_width), int(shape_height)
|
||||
)
|
||||
|
||||
offset_distance = ry * text_offset_ratio
|
||||
|
||||
@@ -177,12 +188,22 @@ def calculate_two_node_shape(
|
||||
|
||||
else: # rectangle
|
||||
shape_width = distance
|
||||
shape_height = max(node1_width, node1_height, node2_width, node2_height)
|
||||
shape_height = max(
|
||||
node1_width, node1_height, node2_width, node2_height
|
||||
)
|
||||
|
||||
svg_x = center_x - ((shape_width / 2) * math.cos(angle_rad) - (shape_height / 2) * math.sin(angle_rad))
|
||||
svg_y = center_y - ((shape_width / 2) * math.sin(angle_rad) + (shape_height / 2) * math.cos(angle_rad))
|
||||
svg_x = center_x - (
|
||||
(shape_width / 2) * math.cos(angle_rad)
|
||||
- (shape_height / 2) * math.sin(angle_rad)
|
||||
)
|
||||
svg_y = center_y - (
|
||||
(shape_width / 2) * math.sin(angle_rad)
|
||||
+ (shape_height / 2) * math.cos(angle_rad)
|
||||
)
|
||||
|
||||
shape_svg = generate_rectangle_svg(int(shape_width), int(shape_height), color_scheme)
|
||||
shape_svg = generate_rectangle_svg(
|
||||
int(shape_width), int(shape_height), color_scheme
|
||||
)
|
||||
|
||||
offset_distance = (shape_height / 2) * text_offset_ratio
|
||||
|
||||
@@ -238,7 +259,7 @@ def generate_ellipse_svg(
|
||||
svg_height: int,
|
||||
) -> str:
|
||||
"""Generate SVG for ellipse."""
|
||||
return f"""<svg width="{svg_width}" height="{svg_height}"><ellipse cx="{rx}" cy="{ry}" rx="{rx}" ry="{ry}" fill="{color_scheme["fill"]}" fill-opacity="{color_scheme["fill_opacity"]}"/></svg>"""
|
||||
return f"""<svg width="{svg_width}" height="{svg_height}"><ellipse cx="{rx}" cy="{ry}" rx="{rx}" ry="{ry}" fill="{color_scheme["fill"]}" fill-opacity="{color_scheme["fill_opacity"]}"/></svg>""" # noqa: E501
|
||||
|
||||
|
||||
def generate_rectangle_svg(
|
||||
@@ -247,7 +268,7 @@ def generate_rectangle_svg(
|
||||
color_scheme: dict[str, Any],
|
||||
) -> str:
|
||||
"""Generate SVG for rectangle."""
|
||||
return f"""<svg width="{width}" height="{height}"><rect x="0" y="0" width="{width}" height="{height}" fill="{color_scheme["fill"]}" fill-opacity="{color_scheme["fill_opacity"]}"/></svg>"""
|
||||
return f"""<svg width="{width}" height="{height}"><rect x="0" y="0" width="{width}" height="{height}" fill="{color_scheme["fill"]}" fill-opacity="{color_scheme["fill_opacity"]}"/></svg>""" # noqa: E501
|
||||
|
||||
|
||||
def generate_text_svg(text: str, color_scheme: dict[str, Any]) -> str:
|
||||
@@ -255,7 +276,7 @@ def generate_text_svg(text: str, color_scheme: dict[str, Any]) -> str:
|
||||
text_width = len(text) * 8 + 20
|
||||
text_height = DEFAULT_FONT_SIZE + 16
|
||||
|
||||
return f"""<svg width="{text_width}" height="{text_height}"><text font-family="TypeWriter" font-size="{DEFAULT_FONT_SIZE}.0" font-weight="bold" fill="{color_scheme["stroke"]}" text-anchor="middle" x="{text_width / 2}" y="{text_height / 2 + 4}">{text}</text></svg>"""
|
||||
return f"""<svg width="{text_width}" height="{text_height}"><text font-family="TypeWriter" font-size="{DEFAULT_FONT_SIZE}.0" font-weight="bold" fill="{color_scheme["stroke"]}" text-anchor="middle" x="{text_width / 2}" y="{text_height / 2 + 4}">{text}</text></svg>""" # noqa: E501
|
||||
|
||||
|
||||
def _hsv_to_hex(h: int, s: int, v: int) -> str:
|
||||
@@ -333,15 +354,33 @@ def _get_color_scheme(area_name: str) -> dict[str, Any]:
|
||||
return COLOR_SCHEMES["NORMAL_AREA"]
|
||||
|
||||
# 3. Logical Isolation
|
||||
if "VRF" in label or "VLAN" in label or "MSTP" in label or "VXLAN" in label or "MPLS" in label:
|
||||
if (
|
||||
"VRF" in label
|
||||
or "VLAN" in label
|
||||
or "MSTP" in label
|
||||
or "VXLAN" in label
|
||||
or "MPLS" in label
|
||||
):
|
||||
return COLOR_SCHEMES["ISOLATION"]
|
||||
|
||||
# 4. High Availability
|
||||
if "VRRP" in label or "HSRP" in label or "HA" in label or "STACK" in label or "M-LAG" in label:
|
||||
if (
|
||||
"VRRP" in label
|
||||
or "HSRP" in label
|
||||
or "HA" in label
|
||||
or "STACK" in label
|
||||
or "M-LAG" in label
|
||||
):
|
||||
return COLOR_SCHEMES["HIGH_AVAILABILITY"]
|
||||
|
||||
# 5. External/Internet
|
||||
if "INET" in label or "OUT" in label or "EXTERNAL" in label or "INTERNET" in label or "DMZ" in label:
|
||||
if (
|
||||
"INET" in label
|
||||
or "OUT" in label
|
||||
or "EXTERNAL" in label
|
||||
or "INTERNET" in label
|
||||
or "DMZ" in label
|
||||
):
|
||||
return COLOR_SCHEMES["EXTERNAL"]
|
||||
|
||||
# 6. Management
|
||||
@@ -353,7 +392,12 @@ def _get_color_scheme(area_name: str) -> dict[str, Any]:
|
||||
return COLOR_SCHEMES["SECURITY_TRUSTED"]
|
||||
|
||||
# 8. Cloud/Tunnel
|
||||
if "TUNNEL" in label or "CLOUD" in label or "GRE" in label or "IPSEC" in label:
|
||||
if (
|
||||
"TUNNEL" in label
|
||||
or "CLOUD" in label
|
||||
or "GRE" in label
|
||||
or "IPSEC" in label
|
||||
):
|
||||
return COLOR_SCHEMES["CLOUD_TUNNEL"]
|
||||
|
||||
# Legacy keyword matching for backward compatibility
|
||||
@@ -392,7 +436,9 @@ def calculate_two_node_ellipse(
|
||||
|
||||
Wrapper around calculate_two_node_shape with shape_type="ellipse".
|
||||
"""
|
||||
result = calculate_two_node_shape(node1, node2, area_name, "ellipse", text_offset_ratio)
|
||||
result = calculate_two_node_shape(
|
||||
node1, node2, area_name, "ellipse", text_offset_ratio
|
||||
)
|
||||
return {
|
||||
"ellipse": result["shape"],
|
||||
"text": result["text"],
|
||||
@@ -411,7 +457,9 @@ def calculate_two_node_rectangle(
|
||||
|
||||
Wrapper around calculate_two_node_shape with shape_type="rectangle".
|
||||
"""
|
||||
result = calculate_two_node_shape(node1, node2, area_name, "rectangle", text_offset_ratio)
|
||||
result = calculate_two_node_shape(
|
||||
node1, node2, area_name, "rectangle", text_offset_ratio
|
||||
)
|
||||
metadata = result["metadata"]
|
||||
return {
|
||||
"rectangle": result["shape"],
|
||||
|
||||
@@ -30,7 +30,9 @@ This module provides utility functions to retrieve LLM model configurations
|
||||
with decrypted API keys by directly accessing the database.
|
||||
|
||||
Usage:
|
||||
from gns3server.agent.gns3_copilot.utils.llm_config_helper import get_user_llm_config_with_app
|
||||
from gns3server.agent.gns3_copilot.utils.llm_config_helper import (
|
||||
get_user_llm_config_with_app,
|
||||
)
|
||||
|
||||
# Get user's default LLM config (with API key)
|
||||
config = await get_user_llm_config_with_app(user_id, app)
|
||||
@@ -51,7 +53,9 @@ from fastapi import FastAPI
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def get_user_llm_config_with_app(user_id: UUID, app: FastAPI) -> Optional[Dict[str, Any]]:
|
||||
async def get_user_llm_config_with_app(
|
||||
user_id: UUID, app: FastAPI
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get user's default LLM model configuration with decrypted API key.
|
||||
|
||||
@@ -91,5 +95,8 @@ async def get_user_llm_config_with_app(user_id: UUID, app: FastAPI) -> Optional[
|
||||
return config
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to retrieve LLM config for user {user_id}: {e}", exc_info=True)
|
||||
logger.error(
|
||||
f"Failed to retrieve LLM config for user {user_id}: {e}",
|
||||
exc_info=True,
|
||||
)
|
||||
return None
|
||||
@@ -55,7 +55,8 @@ def convert_langchain_to_openai(lc_message) -> Dict[str, Any]:
|
||||
Convert LangChain message to OpenAI-compatible format.
|
||||
|
||||
Args:
|
||||
lc_message: LangChain message (HumanMessage, AIMessage, SystemMessage, ToolMessage)
|
||||
lc_message: LangChain message (HumanMessage, AIMessage, SystemMessage,
|
||||
ToolMessage)
|
||||
|
||||
Returns:
|
||||
Dictionary in OpenAI-compatible format
|
||||
@@ -90,7 +91,10 @@ def convert_langchain_to_openai(lc_message) -> Dict[str, Any]:
|
||||
{
|
||||
"id": tc_dict.get("id", str(uuid.uuid4())),
|
||||
"type": "function",
|
||||
"function": {"name": tc_dict.get("name", ""), "arguments": tc_dict.get("args", {})},
|
||||
"function": {
|
||||
"name": tc_dict.get("name", ""),
|
||||
"arguments": tc_dict.get("args", {}),
|
||||
},
|
||||
}
|
||||
)
|
||||
msg["tool_calls"] = tool_calls
|
||||
@@ -149,7 +153,11 @@ def convert_openai_to_langchain(msg: Dict[str, Any]):
|
||||
return ai_msg
|
||||
|
||||
elif role == "tool":
|
||||
return ToolMessage(content=content, name=msg.get("name", ""), tool_call_id=msg.get("tool_call_id", ""))
|
||||
return ToolMessage(
|
||||
content=content,
|
||||
name=msg.get("name", ""),
|
||||
tool_call_id=msg.get("tool_call_id", ""),
|
||||
)
|
||||
|
||||
elif role == "system":
|
||||
return SystemMessage(content=content)
|
||||
@@ -176,7 +184,11 @@ def convert_stream_event_to_openai(event: Dict[str, Any]) -> Dict[str, Any]:
|
||||
content = getattr(chunk, "content", "")
|
||||
|
||||
if content:
|
||||
return {"type": "content", "content": content, "message_id": event.get("metadata", {}).get("msg_id")}
|
||||
return {
|
||||
"type": "content",
|
||||
"content": content,
|
||||
"message_id": event.get("metadata", {}).get("msg_id"),
|
||||
}
|
||||
|
||||
# Check for tool call chunks
|
||||
if hasattr(chunk, "tool_call_chunks") and chunk.tool_call_chunks:
|
||||
@@ -191,12 +203,19 @@ def convert_stream_event_to_openai(event: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"tool_call": {
|
||||
"id": tc_id,
|
||||
"type": "function",
|
||||
"function": {"name": tc_name or "", "arguments": tc_args or ""},
|
||||
"function": {
|
||||
"name": tc_name or "",
|
||||
"arguments": tc_args or "",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
elif event_type == "on_tool_start":
|
||||
return {"type": "tool_start", "tool_name": event.get("name", ""), "metadata": event.get("metadata", {})}
|
||||
return {
|
||||
"type": "tool_start",
|
||||
"tool_name": event.get("name", ""),
|
||||
"metadata": event.get("metadata", {}),
|
||||
}
|
||||
|
||||
elif event_type == "on_tool_end":
|
||||
tool_output = event.get("data", {}).get("output", "")
|
||||
|
||||
@@ -85,12 +85,14 @@ def parse_tool_content(
|
||||
by json.dumps.
|
||||
|
||||
Args:
|
||||
content: Content returned by tools (can be str, dict, list, int, float, bool, or None)
|
||||
content: Content returned by tools (can be str, dict, list, int, float, bool,
|
||||
or None)
|
||||
fallback_to_raw: Whether to return raw content when parsing fails, default True
|
||||
strict_mode: Strict mode, raises exceptions when parsing fails, default False
|
||||
|
||||
Returns:
|
||||
Union[Dict[str, Any], List[Any], Any]: Parsed data that can be serialized by json.dumps:
|
||||
Union[Dict[str, Any], List[Any], Any]: Parsed data that can be serialized by
|
||||
json.dumps:
|
||||
- Successfully parsed JSON/Python literal data
|
||||
- Original dict/list objects (passed through)
|
||||
- Primitive types (int, float, bool, str)
|
||||
@@ -158,7 +160,9 @@ def parse_tool_content(
|
||||
# Empty string handling
|
||||
if not content.strip():
|
||||
result = {}
|
||||
logger.info("Content is empty or whitespace, returning: %s", result)
|
||||
logger.info(
|
||||
"Content is empty or whitespace, returning: %s", result
|
||||
)
|
||||
return result
|
||||
|
||||
s = content.strip()
|
||||
@@ -166,14 +170,19 @@ def parse_tool_content(
|
||||
# Handle empty dictionary case
|
||||
if s == "{}":
|
||||
result = {}
|
||||
logger.info("Content is empty dictionary, returning: %s", result)
|
||||
logger.info(
|
||||
"Content is empty dictionary, returning: %s", result
|
||||
)
|
||||
return result
|
||||
|
||||
# Try to parse as Python literal
|
||||
# (higher priority as many tools return Python format strings)
|
||||
try:
|
||||
result = ast.literal_eval(s)
|
||||
logger.info("Successfully parsed as Python literal, returning: %s", result)
|
||||
logger.info(
|
||||
"Successfully parsed as Python literal, returning: %s",
|
||||
result,
|
||||
)
|
||||
return result
|
||||
except (ValueError, SyntaxError):
|
||||
pass
|
||||
@@ -181,7 +190,9 @@ def parse_tool_content(
|
||||
# Try to parse as JSON
|
||||
try:
|
||||
result = json.loads(s)
|
||||
logger.info("Successfully parsed as JSON, returning: %s", result)
|
||||
logger.info(
|
||||
"Successfully parsed as JSON, returning: %s", result
|
||||
)
|
||||
return result
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
@@ -210,7 +221,8 @@ def parse_tool_content(
|
||||
|
||||
# Handle unsupported types
|
||||
error_msg = ( # type: ignore[unreachable]
|
||||
"Content must be str, dict, list, int, float, bool, or None, got " f"{type(content).__name__}"
|
||||
"Content must be str, dict, list, int, float, bool, or None, got "
|
||||
f"{type(content).__name__}"
|
||||
)
|
||||
logger.error(error_msg)
|
||||
|
||||
@@ -226,7 +238,9 @@ def parse_tool_content(
|
||||
return result
|
||||
|
||||
|
||||
def format_tool_response(content: str | dict | list | int | float | bool | None, indent: int = 2) -> str:
|
||||
def format_tool_response(
|
||||
content: str | dict | list | int | float | bool | None, indent: int = 2
|
||||
) -> str:
|
||||
"""
|
||||
Format tool response as a beautiful JSON string for UI display.
|
||||
|
||||
@@ -234,7 +248,8 @@ def format_tool_response(content: str | dict | list | int | float | bool | None,
|
||||
properly displayed in UI interfaces.
|
||||
|
||||
Args:
|
||||
content: Content returned by tools (can be str, dict, list, int, float, bool, or None)
|
||||
content: Content returned by tools (can be str, dict, list, int, float, bool,
|
||||
or None)
|
||||
indent: JSON indentation spaces, default 2
|
||||
|
||||
Returns:
|
||||
@@ -244,7 +259,9 @@ def format_tool_response(content: str | dict | list | int | float | bool | None,
|
||||
logger.info("format_tool_response parameter indent: %s", indent)
|
||||
|
||||
try:
|
||||
parsed = parse_tool_content(content, fallback_to_raw=True, strict_mode=False)
|
||||
parsed = parse_tool_content(
|
||||
content, fallback_to_raw=True, strict_mode=False
|
||||
)
|
||||
# Ensure the result can be serialized to JSON
|
||||
result = json.dumps(parsed, ensure_ascii=False, indent=indent)
|
||||
logger.info("format_tool_response returning: %s", result)
|
||||
@@ -253,7 +270,9 @@ def format_tool_response(content: str | dict | list | int | float | bool | None,
|
||||
# If the parsed result cannot be serialized, convert to string and wrap
|
||||
logger.error("Cannot serialize parsed result to JSON: %s", e)
|
||||
try:
|
||||
result = json.dumps({"raw": str(content)}, ensure_ascii=False, indent=indent)
|
||||
result = json.dumps(
|
||||
{"raw": str(content)}, ensure_ascii=False, indent=indent
|
||||
)
|
||||
logger.info("format_tool_response returning fallback: %s", result)
|
||||
return result
|
||||
except Exception:
|
||||
@@ -267,12 +286,16 @@ def format_tool_response(content: str | dict | list | int | float | bool | None,
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error formatting tool response: %s", e)
|
||||
result = json.dumps({"error": str(e)}, ensure_ascii=False, indent=indent)
|
||||
result = json.dumps(
|
||||
{"error": str(e)}, ensure_ascii=False, indent=indent
|
||||
)
|
||||
logger.info("format_tool_response returning error: %s", result)
|
||||
return result
|
||||
|
||||
|
||||
def normalize_tool_response(response: dict | list | str, tool_name: str = "unknown") -> dict:
|
||||
def normalize_tool_response(
|
||||
response: dict | list | str, tool_name: str = "unknown"
|
||||
) -> dict:
|
||||
"""
|
||||
Normalize tool response to standard format for consistent frontend display.
|
||||
|
||||
@@ -298,15 +321,24 @@ def normalize_tool_response(response: dict | list | str, tool_name: str = "unkno
|
||||
|
||||
Examples:
|
||||
>>> normalize_tool_response({"status": "success", "output": "OK"})
|
||||
{'success': True, 'total': 1, 'successful': 1, 'failed': 0, 'data': [{'status': 'success', 'result': 'OK'}], 'metadata': {}}
|
||||
{'success': True, 'total': 1, 'successful': 1, 'failed': 0,
|
||||
'data': [{'status': 'success', 'result': 'OK'}], 'metadata': {}}
|
||||
|
||||
>>> normalize_tool_response([{"device_name": "R1", "status": "success"}])
|
||||
{'success': True, 'total': 1, 'successful': 1, 'failed': 0, 'data': [...], 'metadata': {}}
|
||||
{'success': True, 'total': 1, 'successful': 1, 'failed': 0,
|
||||
'data': [...], 'metadata': {}}
|
||||
"""
|
||||
metadata = {"tool_name": tool_name, "normalized_at": datetime.utcnow().isoformat()}
|
||||
metadata = {
|
||||
"tool_name": tool_name,
|
||||
"normalized_at": datetime.utcnow().isoformat(),
|
||||
}
|
||||
|
||||
# Handle error responses
|
||||
if isinstance(response, dict) and "error" in response and len(response) == 1:
|
||||
if (
|
||||
isinstance(response, dict)
|
||||
and "error" in response
|
||||
and len(response) == 1
|
||||
):
|
||||
return {
|
||||
"success": False,
|
||||
"total": 0,
|
||||
@@ -319,29 +351,56 @@ def normalize_tool_response(response: dict | list | str, tool_name: str = "unkno
|
||||
|
||||
# Handle empty responses
|
||||
if not response:
|
||||
return {"success": True, "total": 0, "successful": 0, "failed": 0, "data": [], "metadata": metadata}
|
||||
return {
|
||||
"success": True,
|
||||
"total": 0,
|
||||
"successful": 0,
|
||||
"failed": 0,
|
||||
"data": [],
|
||||
"metadata": metadata,
|
||||
}
|
||||
|
||||
# Handle list responses (most tools return list of device results)
|
||||
if isinstance(response, list):
|
||||
successful = sum(1 for item in response if isinstance(item, dict) and item.get("status") == "success")
|
||||
successful = sum(
|
||||
1
|
||||
for item in response
|
||||
if isinstance(item, dict) and item.get("status") == "success"
|
||||
)
|
||||
failed = len(response) - successful
|
||||
|
||||
normalized_data = []
|
||||
for item in response:
|
||||
if isinstance(item, dict):
|
||||
normalized_item = {
|
||||
"id": item.get("device_id") or item.get("node_id") or item.get("id") or "",
|
||||
"id": item.get("device_id")
|
||||
or item.get("node_id")
|
||||
or item.get("id")
|
||||
or "",
|
||||
"name": item.get("device_name") or item.get("name") or "",
|
||||
"status": item.get("status", "unknown"),
|
||||
}
|
||||
if normalized_item["status"] == "success":
|
||||
normalized_item["result"] = item.get("output") or item.get("result") or ""
|
||||
normalized_item["result"] = (
|
||||
item.get("output") or item.get("result") or ""
|
||||
)
|
||||
else:
|
||||
normalized_item["error"] = item.get("error") or item.get("output") or "Unknown error"
|
||||
normalized_item["error"] = (
|
||||
item.get("error")
|
||||
or item.get("output")
|
||||
or "Unknown error"
|
||||
)
|
||||
normalized_data.append(normalized_item)
|
||||
else:
|
||||
# Non-dict items in list
|
||||
normalized_data.append({"id": "", "name": "", "status": "unknown", "result": str(item)})
|
||||
normalized_data.append(
|
||||
{
|
||||
"id": "",
|
||||
"name": "",
|
||||
"status": "unknown",
|
||||
"result": str(item),
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"success": failed == 0,
|
||||
@@ -367,8 +426,14 @@ def normalize_tool_response(response: dict | list | str, tool_name: str = "unkno
|
||||
}
|
||||
|
||||
# Legacy format: extract common fields
|
||||
total = response.get("total_nodes") or response.get("total") or response.get("count", 1)
|
||||
successful = response.get("successful_nodes") or response.get("successful") or 0
|
||||
total = (
|
||||
response.get("total_nodes")
|
||||
or response.get("total")
|
||||
or response.get("count", 1)
|
||||
)
|
||||
successful = (
|
||||
response.get("successful_nodes") or response.get("successful") or 0
|
||||
)
|
||||
failed = response.get("failed_nodes") or response.get("failed") or 0
|
||||
|
||||
# Extract data from various possible locations
|
||||
@@ -393,9 +458,13 @@ def normalize_tool_response(response: dict | list | str, tool_name: str = "unkno
|
||||
if not data and "status" in response:
|
||||
data = [
|
||||
{
|
||||
"name": response.get("device_name") or response.get("name") or "",
|
||||
"name": response.get("device_name")
|
||||
or response.get("name")
|
||||
or "",
|
||||
"status": response["status"],
|
||||
"result": response.get("output") or response.get("result") or "",
|
||||
"result": response.get("output")
|
||||
or response.get("result")
|
||||
or "",
|
||||
"error": response.get("error") or "",
|
||||
}
|
||||
]
|
||||
@@ -425,14 +494,23 @@ def normalize_tool_response(response: dict | list | str, tool_name: str = "unkno
|
||||
"total": 1,
|
||||
"successful": 1,
|
||||
"failed": 0,
|
||||
"data": [{"id": "", "name": "", "status": "unknown", "result": str(response)}],
|
||||
"data": [
|
||||
{
|
||||
"id": "",
|
||||
"name": "",
|
||||
"status": "unknown",
|
||||
"result": str(response),
|
||||
}
|
||||
],
|
||||
"metadata": metadata,
|
||||
}
|
||||
|
||||
|
||||
# Test function to verify the implementation
|
||||
def _test_parse_tool_content() -> None:
|
||||
"""Test function to verify parse_tool_content works correctly with all input types"""
|
||||
"""Test function to verify parse_tool_content works correctly with all input
|
||||
types
|
||||
"""
|
||||
test_cases: list[tuple[Any, Any]] = [
|
||||
# String inputs
|
||||
(
|
||||
@@ -486,7 +564,9 @@ def _test_parse_tool_content() -> None:
|
||||
valid = "✓"
|
||||
except Exception:
|
||||
valid = "✗"
|
||||
print(f"Format Test {i + 1}: {valid} Input: {repr(input_data)} -> {result}")
|
||||
print(
|
||||
f"Format Test {i + 1}: {valid} Input: {repr(input_data)} -> {result}"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -79,10 +79,11 @@ class ToolCallStreamAccumulator:
|
||||
tc_id = getattr(tool_call, "id", None)
|
||||
tc_name = getattr(tool_call, "name", "")
|
||||
|
||||
# Only when ID is not empty, consider it as the start of a new tool call
|
||||
# Only when ID is not empty, consider it as the start of a new
|
||||
# tool call
|
||||
if tc_id:
|
||||
# Initialize current tool state (this is the only time to get ID)
|
||||
# Note: only one tool can be called at a time
|
||||
# Initialize current tool state (this is the only time to
|
||||
# get ID). Note: only one tool can be called at a time
|
||||
self._current_tool_call = {
|
||||
"id": tc_id,
|
||||
"name": tc_name if tc_name else "UNKNOWN_TOOL",
|
||||
@@ -90,19 +91,24 @@ class ToolCallStreamAccumulator:
|
||||
}
|
||||
|
||||
# Send initial tool_call event with empty args
|
||||
chunks.append({
|
||||
"type": "tool_call",
|
||||
"tool_call": {
|
||||
"id": tc_id,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": self._current_tool_call["name"],
|
||||
"arguments": ""
|
||||
}
|
||||
chunks.append(
|
||||
{
|
||||
"type": "tool_call",
|
||||
"tool_call": {
|
||||
"id": tc_id,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": self._current_tool_call[
|
||||
"name"
|
||||
],
|
||||
"arguments": "",
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
# ========== Phase 2: Concatenate parameter strings from tool_call_chunks ==========
|
||||
# ========== Phase 2: Concatenate parameter strings from
|
||||
# tool_call_chunks ==========
|
||||
# Concatenate parameter strings from tool_call_chunk
|
||||
if hasattr(chunk, "tool_call_chunks") and chunk.tool_call_chunks:
|
||||
if self._current_tool_call:
|
||||
@@ -116,43 +122,58 @@ class ToolCallStreamAccumulator:
|
||||
|
||||
# Core: string concatenation
|
||||
if isinstance(args_chunk, str):
|
||||
tool_data["args_string"] += args_chunk
|
||||
tool_data[
|
||||
"args_string"
|
||||
] += args_chunk
|
||||
|
||||
# Send updated tool_call event with accumulated args
|
||||
chunks.append({
|
||||
"type": "tool_call",
|
||||
"tool_call": {
|
||||
"id": tool_data["id"],
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool_data["name"],
|
||||
"arguments": tool_data["args_string"]
|
||||
}
|
||||
chunks.append(
|
||||
{
|
||||
"type": "tool_call",
|
||||
"tool_call": {
|
||||
"id": tool_data["id"],
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool_data["name"],
|
||||
"arguments": tool_data[
|
||||
"args_string"
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
# ========== Phase 3: Determine if tool_calls_chunks output is complete ==========
|
||||
# ========== Phase 3: Determine if tool_calls_chunks output is
|
||||
# complete ==========
|
||||
# Check finish_reason == "tool_calls" or "STOP"
|
||||
response_metadata = getattr(chunk, "response_metadata", {})
|
||||
finish_reason = response_metadata.get("finish_reason") if isinstance(response_metadata, dict) else None
|
||||
finish_reason = (
|
||||
response_metadata.get("finish_reason")
|
||||
if isinstance(response_metadata, dict)
|
||||
else None
|
||||
)
|
||||
|
||||
if (finish_reason == "tool_calls") or (finish_reason == "stop" and self._current_tool_call is not None):
|
||||
if (finish_reason == "tool_calls") or (
|
||||
finish_reason == "stop" and self._current_tool_call is not None
|
||||
):
|
||||
if self._current_tool_call:
|
||||
tool_data = self._current_tool_call
|
||||
|
||||
# Send final complete tool_call event
|
||||
chunks.append({
|
||||
"type": "tool_call",
|
||||
"tool_call": {
|
||||
"id": tool_data["id"],
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool_data["name"],
|
||||
"arguments": tool_data["args_string"],
|
||||
"complete": True # Mark as complete
|
||||
}
|
||||
chunks.append(
|
||||
{
|
||||
"type": "tool_call",
|
||||
"tool_call": {
|
||||
"id": tool_data["id"],
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool_data["name"],
|
||||
"arguments": tool_data["args_string"],
|
||||
"complete": True, # Mark as complete
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
# Clear the current tool call state
|
||||
self._current_tool_call = None
|
||||
@@ -160,11 +181,13 @@ class ToolCallStreamAccumulator:
|
||||
# Also handle regular content (when not in tool call mode)
|
||||
content = getattr(chunk, "content", "")
|
||||
if content and not self._current_tool_call:
|
||||
chunks.append({
|
||||
"type": "content",
|
||||
"content": content,
|
||||
"message_id": event.get("metadata", {}).get("msg_id")
|
||||
})
|
||||
chunks.append(
|
||||
{
|
||||
"type": "content",
|
||||
"content": content,
|
||||
"message_id": event.get("metadata", {}).get("msg_id"),
|
||||
}
|
||||
)
|
||||
|
||||
return chunks
|
||||
|
||||
|
||||
Reference in new issue
Block a user