Skip to content
Open
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
77 changes: 68 additions & 9 deletions src/cuga/backend/cuga_graph/nodes/cuga_lite/prompt_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,15 +252,23 @@ async def find_tools(
llm_manager = LLMManager()
model = llm or llm_manager.get_model(settings.agent.code.model)
chain = BaseAgent.get_chain(prompt, model, ShortListerOutputLite)
response = await chain.ainvoke(
{
"input": query,
"all_apps": apps_as_dict,
"all_tools": tools_as_dict,
"instructions": "",
"memory": None,
}
)
try:
response = await chain.ainvoke(
{
"input": query,
"all_apps": apps_as_dict,
"all_tools": tools_as_dict,
"instructions": "",
"memory": None,
}
)
except Exception as e:
logger.bind(
Copy link
Contributor

Choose a reason for hiding this comment

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

What is the reason this could fail here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The reason it failed for OP was that the shortlister returned None and the JSON Decoder threw an OutputParserException. This is passed through to here, where this code will catch it and any other excpeptions. This is a sample stack trace:

Error during execution: OutputParserException('Invalid json output: \nFor troubleshooting, visit: https://docs.langchain.com/oss/python/langchain/errors/OUTPUT_PARSING_FAILURE ')
Traceback (most recent call last):
  File "/root/proj/cuga-internal-evaluation/.venv/lib/python3.13/site-packages/langchain_core/output_parsers/json.py", line 84, in parse_result
    return parse_json_markdown(text)
  File "/root/proj/cuga-internal-evaluation/.venv/lib/python3.13/site-packages/langchain_core/utils/json.py", line 164, in parse_json_markdown
    return _parse_json(json_str, parser=parser)
  File "/root/proj/cuga-internal-evaluation/.venv/lib/python3.13/site-packages/langchain_core/utils/json.py", line 194, in _parse_json
    return parser(json_str)
  File "/root/proj/cuga-internal-evaluation/.venv/lib/python3.13/site-packages/langchain_core/utils/json.py", line 137, in parse_partial_json
    return json.loads(s, strict=strict)
           ~~~~~~~~~~^^^^^^^^^^^^^^^^^^
  File "/root/.local/share/uv/python/cpython-3.13.12-linux-x86_64-gnu/lib/python3.13/json/__init__.py", line 365, in loads
    return cls(**kw).decode(s)
           ~~~~~~~~~~~~~~~~^^^
  File "/root/.local/share/uv/python/cpython-3.13.12-linux-x86_64-gnu/lib/python3.13/json/decoder.py", line 345, in decode
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
               ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
  File "/root/.local/share/uv/python/cpython-3.13.12-linux-x86_64-gnu/lib/python3.13/json/decoder.py", line 363, in raw_decode
    raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/root/proj/cuga-internal-evaluation/vendor/cuga-agent/src/cuga/backend/cuga_graph/nodes/cuga_lite/executors/code_executor.py", line 121, in eval_with_tools_async
    result = await executor.execute(
             ^^^^^^^^^^^^^^^^^^^^^^^
    ...<3 lines>...
    )
    ^
  File "/root/proj/cuga-internal-evaluation/vendor/cuga-agent/src/cuga/backend/cuga_graph/nodes/cuga_lite/executors/local/local_executor.py", line 80, in execute
    result_locals = await asyncio.wait_for(async_main(), timeout=timeout)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/root/.local/share/uv/python/cpython-3.13.12-linux-x86_64-gnu/lib/python3.13/asyncio/tasks.py", line 507, in wait_for
    return await fut
           ^^^^^^^^^
  File "<string>", line 5, in _async_main
  File "/root/proj/cuga-internal-evaluation/vendor/cuga-agent/src/cuga/backend/cuga_graph/nodes/cuga_lite/cuga_lite_graph.py", line 121, in wrapper_with_pydantic
    result = await func(*args, **kwargs) if inspect.iscoroutinefunction(func) else func(*args, **kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/root/proj/cuga-internal-evaluation/vendor/cuga-agent/src/cuga/backend/cuga_graph/nodes/cuga_lite/cuga_lite_graph.py", line 375, in find_tools_func
    return await PromptUtils.find_tools(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        query=query, all_tools=filtered_tools, all_apps=filtered_apps, llm=llm

query_len=len(query),
error_type=type(e).__name__,
).opt(exception=True).warning("Tool shortlisting failed; using fallback tool list")
# Return all tools unfiltered so the agent can still proceed
return PromptUtils._format_all_tools_as_fallback(all_tools)

enriched_tools = []
for api_detail in response.result:
Expand Down Expand Up @@ -364,6 +372,57 @@ async def find_tools(

return "\n".join(markdown_lines)

@staticmethod
def _format_all_tools_as_fallback(all_tools: List[StructuredTool]) -> str:
"""Format all tools as a fallback when shortlisting fails.

Returns a markdown string listing all available tools so the agent can still proceed.
Output is bounded to prevent context-overflow failures.
"""
if not all_tools:
return "No matching tools found for your query."

max_tools = 20
max_doc_chars = 1200
shown_tools = all_tools[:max_tools]

markdown_lines = [
f"# Available Tools ({len(all_tools)} total)\n",
"**Note:** Tool shortlisting was unavailable. Showing all tools.\n",
]
if len(all_tools) > max_tools:
markdown_lines.append(
f"**Output truncated:** showing first {max_tools} of {len(all_tools)} tools.\n"
)

for idx, tool in enumerate(shown_tools, 1):
markdown_lines.append(f"## {idx}. `{tool.name}`\n")
if hasattr(tool, 'description') and tool.description:
markdown_lines.append(f"**Description:** {tool.description}\n")

try:
params_doc, response_doc = PromptUtils.get_tool_docs(tool)
except Exception:
logger.bind(
tool_name=getattr(tool, "name", "<unknown>"),
).opt(exception=True).warning("Tool doc extraction failed during fallback; skipping docs")
params_doc, response_doc = "", ""
if params_doc:
if len(params_doc) > max_doc_chars:
params_doc = params_doc[:max_doc_chars] + "\n... (truncated)"
markdown_lines.append("**Parameters:**\n")
markdown_lines.append(f"{params_doc}\n")

if response_doc:
if len(response_doc) > max_doc_chars:
response_doc = response_doc[:max_doc_chars] + "\n... (truncated)"
markdown_lines.append("**Response Schema:**\n")
markdown_lines.append(f"{response_doc}\n")

markdown_lines.append("---\n")

return "\n".join(markdown_lines)

@staticmethod
def create_find_tools_bound(all_tools: List[StructuredTool], all_apps: List[AppDefinition]):
"""Create a bound version of find_tools with all_tools and all_apps pre-bound.
Expand Down
Loading