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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,6 @@ pyrightconfig.json
mypy.ini
tox.ini
*.difypkg

.claude
dify-plugin.exe
2 changes: 1 addition & 1 deletion agent-strategies/cot_agent/manifest.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version: 0.0.25
version: 0.0.26
type: plugin
author: "langgenius"
name: "agent"
Expand Down
122 changes: 76 additions & 46 deletions agent-strategies/cot_agent/strategies/ReAct.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@
from prompt.template import REACT_PROMPT_TEMPLATES
from pydantic import BaseModel, Field


class LogMetadata:
"""Metadata keys for logging"""

STARTED_AT = "started_at"
PROVIDER = "provider"
FINISHED_AT = "finished_at"
Expand All @@ -38,6 +40,7 @@ class LogMetadata:
CURRENCY = "currency"
TOTAL_TOKENS = "total_tokens"


ignore_observation_providers = ["wenxin"]


Expand Down Expand Up @@ -250,15 +253,15 @@ def _invoke(self, parameters: dict[str, Any]) -> Generator[AgentInvokeMessage]:
LogMetadata.FINISHED_AT: time.perf_counter(),
LogMetadata.ELAPSED_TIME: time.perf_counter() - model_started_at,
LogMetadata.PROVIDER: model.provider,
LogMetadata.TOTAL_PRICE: usage_dict["usage"].total_price
if usage_dict["usage"]
else 0,
LogMetadata.CURRENCY: usage_dict["usage"].currency
if usage_dict["usage"]
else "",
LogMetadata.TOTAL_TOKENS: usage_dict["usage"].total_tokens
if usage_dict["usage"]
else 0,
LogMetadata.TOTAL_PRICE: (
usage_dict["usage"].total_price if usage_dict["usage"] else 0
),
LogMetadata.CURRENCY: (
usage_dict["usage"].currency if usage_dict["usage"] else ""
),
LogMetadata.TOTAL_TOKENS: (
usage_dict["usage"].total_tokens if usage_dict["usage"] else 0
),
},
)
if not scratchpad.action:
Expand All @@ -285,11 +288,11 @@ def _invoke(self, parameters: dict[str, Any]) -> Generator[AgentInvokeMessage]:
data={},
metadata={
LogMetadata.STARTED_AT: time.perf_counter(),
LogMetadata.PROVIDER: tool_instances[
tool_name
].identity.provider
if tool_instances.get(tool_name)
else "",
LogMetadata.PROVIDER: (
tool_instances[tool_name].identity.provider
if tool_instances.get(tool_name)
else ""
),
},
parent=round_log,
status=ToolInvokeMessage.LogMessage.LogStatus.START,
Expand Down Expand Up @@ -318,11 +321,11 @@ def _invoke(self, parameters: dict[str, Any]) -> Generator[AgentInvokeMessage]:
},
metadata={
LogMetadata.STARTED_AT: tool_call_started_at,
LogMetadata.PROVIDER: tool_instances[
tool_name
].identity.provider
if tool_instances.get(tool_name)
else "",
LogMetadata.PROVIDER: (
tool_instances[tool_name].identity.provider
if tool_instances.get(tool_name)
else ""
),
LogMetadata.FINISHED_AT: time.perf_counter(),
LogMetadata.ELAPSED_TIME: time.perf_counter()
- tool_call_started_at,
Expand All @@ -337,28 +340,28 @@ def _invoke(self, parameters: dict[str, Any]) -> Generator[AgentInvokeMessage]:
yield self.finish_log_message(
log=round_log,
data={
"action_name": scratchpad.action.action_name
if scratchpad.action
else "",
"action_input": scratchpad.action.action_input
if scratchpad.action
else "",
"action_name": (
scratchpad.action.action_name if scratchpad.action else ""
),
"action_input": (
scratchpad.action.action_input if scratchpad.action else ""
),
"thought": scratchpad.thought,
"observation": scratchpad.observation,
},
metadata={
LogMetadata.STARTED_AT: round_started_at,
LogMetadata.FINISHED_AT: time.perf_counter(),
LogMetadata.ELAPSED_TIME: time.perf_counter() - round_started_at,
LogMetadata.TOTAL_PRICE: usage_dict["usage"].total_price
if usage_dict["usage"]
else 0,
LogMetadata.CURRENCY: usage_dict["usage"].currency
if usage_dict["usage"]
else "",
LogMetadata.TOTAL_TOKENS: usage_dict["usage"].total_tokens
if usage_dict["usage"]
else 0,
LogMetadata.TOTAL_PRICE: (
usage_dict["usage"].total_price if usage_dict["usage"] else 0
),
LogMetadata.CURRENCY: (
usage_dict["usage"].currency if usage_dict["usage"] else ""
),
LogMetadata.TOTAL_TOKENS: (
usage_dict["usage"].total_tokens if usage_dict["usage"] else 0
),
},
)
iteration_step += 1
Expand Down Expand Up @@ -395,15 +398,21 @@ def _invoke(self, parameters: dict[str, Any]) -> Generator[AgentInvokeMessage]:
yield self.create_json_message(
{
"execution_metadata": {
LogMetadata.TOTAL_PRICE: llm_usage["usage"].total_price
if llm_usage["usage"] is not None
else 0,
LogMetadata.CURRENCY: llm_usage["usage"].currency
if llm_usage["usage"] is not None
else "",
LogMetadata.TOTAL_TOKENS: llm_usage["usage"].total_tokens
if llm_usage["usage"] is not None
else 0,
LogMetadata.TOTAL_PRICE: (
llm_usage["usage"].total_price
if llm_usage["usage"] is not None
else 0
),
LogMetadata.CURRENCY: (
llm_usage["usage"].currency
if llm_usage["usage"] is not None
else ""
),
LogMetadata.TOTAL_TOKENS: (
llm_usage["usage"].total_tokens
if llm_usage["usage"] is not None
else 0
),
}
}
)
Expand Down Expand Up @@ -516,9 +525,30 @@ def _handle_invoke_action(
)
result = ""
additional_messages = [] # Collect messages that need to be yielded
for response in tool_invoke_responses:
# Collect all responses first to detect duplicates
responses_list = list(tool_invoke_responses)
# Check if there's a JSON response
has_json_response = any(
r.type == ToolInvokeMessage.MessageType.JSON for r in responses_list
)
json_content = None
if has_json_response:
# Get the JSON content for comparison
for r in responses_list:
if r.type == ToolInvokeMessage.MessageType.JSON:
json_content = json.dumps(
cast(
ToolInvokeMessage.JsonMessage,
r.message,
).json_object,
ensure_ascii=False,
)
break
Comment on lines +534 to +546
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The variable json_content is assigned here but it's never used later in the code. This block appears to be dead code and can be removed to simplify the logic. The has_json_response check is sufficient for the deduplication logic.


for response in responses_list:
if response.type == ToolInvokeMessage.MessageType.TEXT:
result += cast(ToolInvokeMessage.TextMessage, response.message).text
# Skip TEXT response as workflow always returns both TEXT and JSON
continue
Comment on lines 549 to +551
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Unconditionally skipping all TEXT type responses will cause problems for tools that only return a text response, as their output would be completely ignored. The logic should be to skip the TEXT response only if a JSON response is also present. When no JSON response exists, the TEXT response should be processed as before.

                if response.type == ToolInvokeMessage.MessageType.TEXT:
                    if has_json_response:
                        # Skip TEXT response as a JSON response exists (likely duplicate content)
                        continue
                    result += cast(ToolInvokeMessage.TextMessage, response.message).text

elif response.type == ToolInvokeMessage.MessageType.LINK:
result += (
f"result link: {cast(ToolInvokeMessage.TextMessage, response.message).text}."
Expand Down Expand Up @@ -546,7 +576,7 @@ def _handle_invoke_action(
).json_object,
ensure_ascii=False,
)
result += f"tool response: {text}."
result += text
elif response.type == ToolInvokeMessage.MessageType.BLOB:
result += "Generated file with ... "
additional_messages.append(response)
Expand Down
98 changes: 61 additions & 37 deletions agent-strategies/cot_agent/strategies/function_calling.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@
)
from pydantic import BaseModel


class LogMetadata:
"""Metadata keys for logging"""

STARTED_AT = "started_at"
PROVIDER = "provider"
FINISHED_AT = "finished_at"
Expand All @@ -39,8 +41,10 @@ class LogMetadata:
CURRENCY = "currency"
TOTAL_TOKENS = "total_tokens"


class ExecutionMetadata(BaseModel):
"""Execution metadata with default values"""

total_price: float = 0.0
currency: str = ""
total_tokens: int = 0
Expand All @@ -53,13 +57,13 @@ class ExecutionMetadata(BaseModel):
completion_price_unit: float = 0.0
completion_price: float = 0.0
latency: float = 0.0

@classmethod
def from_llm_usage(cls, usage: Optional[LLMUsage]) -> "ExecutionMetadata":
"""Create ExecutionMetadata from LLMUsage, handling None case"""
if usage is None:
return cls()

return cls(
total_price=float(usage.total_price),
currency=usage.currency,
Expand All @@ -72,9 +76,10 @@ def from_llm_usage(cls, usage: Optional[LLMUsage]) -> "ExecutionMetadata":
completion_unit_price=float(usage.completion_unit_price),
completion_price_unit=float(usage.completion_price_unit),
completion_price=float(usage.completion_price),
latency=usage.latency
latency=usage.latency,
)


class ContextItem(BaseModel):
content: str
title: str
Expand Down Expand Up @@ -284,15 +289,15 @@ def _invoke(
LogMetadata.FINISHED_AT: time.perf_counter(),
LogMetadata.ELAPSED_TIME: time.perf_counter() - model_started_at,
LogMetadata.PROVIDER: model.provider,
LogMetadata.TOTAL_PRICE: current_llm_usage.total_price
if current_llm_usage
else 0,
LogMetadata.CURRENCY: current_llm_usage.currency
if current_llm_usage
else "",
LogMetadata.TOTAL_TOKENS: current_llm_usage.total_tokens
if current_llm_usage
else 0,
LogMetadata.TOTAL_PRICE: (
current_llm_usage.total_price if current_llm_usage else 0
),
LogMetadata.CURRENCY: (
current_llm_usage.currency if current_llm_usage else ""
),
LogMetadata.TOTAL_TOKENS: (
current_llm_usage.total_tokens if current_llm_usage else 0
),
},
)

Expand Down Expand Up @@ -359,15 +364,34 @@ def _invoke(
},
)
tool_result = ""
for tool_invoke_response in tool_invoke_responses:
# Collect all responses first to detect duplicates
responses_list = list(tool_invoke_responses)
# Check if there's a JSON response
has_json_response = any(
r.type == ToolInvokeMessage.MessageType.JSON
for r in responses_list
)
json_content = None
if has_json_response:
# Get the JSON content for comparison
for r in responses_list:
if r.type == ToolInvokeMessage.MessageType.JSON:
json_content = json.dumps(
cast(
ToolInvokeMessage.JsonMessage,
r.message,
).json_object,
ensure_ascii=False,
)
break
Comment on lines +374 to +386
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The variable json_content is assigned here but it's never used later in the code. This block appears to be dead code and can be removed to improve code clarity and avoid confusion. The has_json_response check is sufficient for the deduplication logic.


for tool_invoke_response in responses_list:
if (
tool_invoke_response.type
== ToolInvokeMessage.MessageType.TEXT
):
tool_result += cast(
ToolInvokeMessage.TextMessage,
tool_invoke_response.message,
).text
# Skip TEXT response as workflow always returns both TEXT and JSON
continue
Comment on lines 390 to +394
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Unconditionally skipping all TEXT type responses will cause problems for tools that only return a text response, as their output would be completely ignored. The logic should be to skip the TEXT response only if a JSON response is also present. When no JSON response exists, the TEXT response should be processed as before.

                            if (
                                tool_invoke_response.type
                                == ToolInvokeMessage.MessageType.TEXT
                            ):
                                if has_json_response:
                                    # Skip TEXT response as a JSON response exists (likely duplicate content)
                                    continue
                                tool_result += cast(
                                    ToolInvokeMessage.TextMessage,
                                    tool_invoke_response.message,
                                ).text

elif (
tool_invoke_response.type
== ToolInvokeMessage.MessageType.LINK
Expand Down Expand Up @@ -432,7 +456,7 @@ def _invoke(
).json_object,
ensure_ascii=False,
)
tool_result += f"tool response: {text}."
tool_result += text
elif (
tool_invoke_response.type
== ToolInvokeMessage.MessageType.BLOB
Expand Down Expand Up @@ -500,15 +524,15 @@ def _invoke(
LogMetadata.STARTED_AT: round_started_at,
LogMetadata.FINISHED_AT: time.perf_counter(),
LogMetadata.ELAPSED_TIME: time.perf_counter() - round_started_at,
LogMetadata.TOTAL_PRICE: current_llm_usage.total_price
if current_llm_usage
else 0,
LogMetadata.CURRENCY: current_llm_usage.currency
if current_llm_usage
else "",
LogMetadata.TOTAL_TOKENS: current_llm_usage.total_tokens
if current_llm_usage
else 0,
LogMetadata.TOTAL_PRICE: (
current_llm_usage.total_price if current_llm_usage else 0
),
LogMetadata.CURRENCY: (
current_llm_usage.currency if current_llm_usage else ""
),
LogMetadata.TOTAL_TOKENS: (
current_llm_usage.total_tokens if current_llm_usage else 0
),
},
)
# If max_iteration_steps=1, need to return tool responses
Expand Down Expand Up @@ -545,11 +569,7 @@ def _invoke(
)

metadata = ExecutionMetadata.from_llm_usage(llm_usage["usage"])
yield self.create_json_message(
{
"execution_metadata": metadata.model_dump()
}
)
yield self.create_json_message({"execution_metadata": metadata.model_dump()})

def check_tool_calls(self, llm_result_chunk: LLMResultChunk) -> bool:
"""
Expand Down Expand Up @@ -648,11 +668,15 @@ def _clear_user_prompt_image_messages(
):
prompt_message.content = "\n".join(
[
content.data
if content.type == PromptMessageContentType.TEXT
else "[image]"
if content.type == PromptMessageContentType.IMAGE
else "[file]"
(
content.data
if content.type == PromptMessageContentType.TEXT
else (
"[image]"
if content.type == PromptMessageContentType.IMAGE
else "[file]"
)
)
for content in prompt_message.content
]
)
Expand Down