arcade docs: add support for GPT-5 series; improve error handling (#529)
Adds support for GPT-5 series of models in `arcade docs`. Improves error handling when the LLM does not generate a valid JSON for a given tool sample inputs. Instead of raising an exception, the CLI uses an empty input, moves on to the next tool, and prints a warning message asking the user the fill in the input sample manually in Javascript and Python files. This PR also moves the Enumerations from a separate `reference.mdx` file to the main toolkit file, as requested by @EricGustin to simplify the docs structure.
This commit is contained in:
parent
a85fa76997
commit
19c1e18a8a
4 changed files with 173 additions and 145 deletions
|
|
@ -820,13 +820,12 @@ def docs(
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
openai_model: str = typer.Option(
|
openai_model: str = typer.Option(
|
||||||
"gpt-4o-mini",
|
"gpt-5-mini",
|
||||||
"--openai-model",
|
"--openai-model",
|
||||||
"-m",
|
"-m",
|
||||||
help=(
|
help=(
|
||||||
"A few parts of the documentation are generated using OpenAI API. "
|
"A few parts of the documentation are generated using OpenAI API. "
|
||||||
"This argument controls which OpenAI model to use. "
|
"Choose one of the 'gpt-4o' and 'gpt-5' series models."
|
||||||
"E.g. 'gpt-4o', 'gpt-4o-mini'."
|
|
||||||
),
|
),
|
||||||
show_default=True,
|
show_default=True,
|
||||||
),
|
),
|
||||||
|
|
@ -845,6 +844,14 @@ def docs(
|
||||||
),
|
),
|
||||||
debug: bool = typer.Option(False, "--debug", "-d", help="Show debug information"),
|
debug: bool = typer.Option(False, "--debug", "-d", help="Show debug information"),
|
||||||
) -> None:
|
) -> None:
|
||||||
|
if not openai_model.startswith("gpt-4o") and not openai_model.startswith("gpt-5"):
|
||||||
|
console.print(
|
||||||
|
f"Attention: '{openai_model}' is not a valid OpenAI model. "
|
||||||
|
"Please choose one of the 'gpt-4o' and 'gpt-5' series models.",
|
||||||
|
style="bold red",
|
||||||
|
)
|
||||||
|
raise typer.Exit()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
success = generate_toolkit_docs(
|
success = generate_toolkit_docs(
|
||||||
console=console,
|
console=console,
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ from rich.console import Console
|
||||||
from arcade_cli.toolkit_docs.docs_builder import (
|
from arcade_cli.toolkit_docs.docs_builder import (
|
||||||
build_example_path,
|
build_example_path,
|
||||||
build_examples,
|
build_examples,
|
||||||
build_reference_mdx_path,
|
|
||||||
build_toolkit_mdx,
|
build_toolkit_mdx,
|
||||||
build_toolkit_mdx_path,
|
build_toolkit_mdx_path,
|
||||||
)
|
)
|
||||||
|
|
@ -58,8 +57,7 @@ def generate_toolkit_docs(
|
||||||
enums = get_all_enumerations(toolkit_dir)
|
enums = get_all_enumerations(toolkit_dir)
|
||||||
|
|
||||||
print_debug(f"Building /{toolkit_name.lower()}.mdx file")
|
print_debug(f"Building /{toolkit_name.lower()}.mdx file")
|
||||||
reference_mdx, toolkit_mdx = build_toolkit_mdx(
|
toolkit_mdx = build_toolkit_mdx(
|
||||||
toolkit_dir=toolkit_dir,
|
|
||||||
tools=tools,
|
tools=tools,
|
||||||
docs_section=docs_section,
|
docs_section=docs_section,
|
||||||
enums=enums,
|
enums=enums,
|
||||||
|
|
@ -69,13 +67,6 @@ def generate_toolkit_docs(
|
||||||
toolkit_mdx_path = build_toolkit_mdx_path(docs_section, docs_dir, toolkit_name)
|
toolkit_mdx_path = build_toolkit_mdx_path(docs_section, docs_dir, toolkit_name)
|
||||||
write_file(toolkit_mdx_path, toolkit_mdx)
|
write_file(toolkit_mdx_path, toolkit_mdx)
|
||||||
|
|
||||||
if reference_mdx:
|
|
||||||
print_debug(f"Building /{toolkit_name.lower()}/reference.mdx file")
|
|
||||||
reference_mdx_path = build_reference_mdx_path(docs_section, docs_dir, toolkit_name)
|
|
||||||
write_file(reference_mdx_path, reference_mdx)
|
|
||||||
else:
|
|
||||||
print_debug("No Enums referenced by tool interfaces. Skipping reference.mdx file")
|
|
||||||
|
|
||||||
if tool_call_examples:
|
if tool_call_examples:
|
||||||
print_debug("Building tool-call examples in Python and JavaScript")
|
print_debug("Building tool-call examples in Python and JavaScript")
|
||||||
examples = build_examples(print_debug, tools, openai_model)
|
examples = build_examples(print_debug, tools, openai_model)
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ from arcade_core.schema import (
|
||||||
ToolInput,
|
ToolInput,
|
||||||
ToolSecretRequirement,
|
ToolSecretRequirement,
|
||||||
)
|
)
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
from arcade_cli.toolkit_docs.templates import (
|
from arcade_cli.toolkit_docs.templates import (
|
||||||
ENUM_ITEM,
|
ENUM_ITEM,
|
||||||
|
|
@ -40,6 +41,8 @@ from arcade_cli.toolkit_docs.utils import (
|
||||||
pascal_to_snake_case,
|
pascal_to_snake_case,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
def build_toolkit_mdx_path(docs_section: str, docs_root_dir: str, toolkit_name: str) -> str:
|
def build_toolkit_mdx_path(docs_section: str, docs_root_dir: str, toolkit_name: str) -> str:
|
||||||
return os.path.join(
|
return os.path.join(
|
||||||
|
|
@ -51,17 +54,6 @@ def build_toolkit_mdx_path(docs_section: str, docs_root_dir: str, toolkit_name:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def build_reference_mdx_path(docs_section: str, docs_root_dir: str, toolkit_name: str) -> str:
|
|
||||||
return os.path.join(
|
|
||||||
docs_root_dir,
|
|
||||||
"pages",
|
|
||||||
"toolkits",
|
|
||||||
docs_section,
|
|
||||||
toolkit_name.lower(),
|
|
||||||
"reference.mdx",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def build_example_path(example_filename: str, docs_root_dir: str, toolkit_name: str) -> str:
|
def build_example_path(example_filename: str, docs_root_dir: str, toolkit_name: str) -> str:
|
||||||
return os.path.join(
|
return os.path.join(
|
||||||
docs_root_dir,
|
docs_root_dir,
|
||||||
|
|
@ -75,7 +67,6 @@ def build_example_path(example_filename: str, docs_root_dir: str, toolkit_name:
|
||||||
|
|
||||||
|
|
||||||
def build_toolkit_mdx(
|
def build_toolkit_mdx(
|
||||||
toolkit_dir: str,
|
|
||||||
tools: list[ToolDefinition],
|
tools: list[ToolDefinition],
|
||||||
docs_section: str,
|
docs_section: str,
|
||||||
enums: dict[str, type[Enum]],
|
enums: dict[str, type[Enum]],
|
||||||
|
|
@ -102,16 +93,20 @@ def build_toolkit_mdx(
|
||||||
)
|
)
|
||||||
table_of_contents = build_table_of_contents(tools)
|
table_of_contents = build_table_of_contents(tools)
|
||||||
footer = build_footer(toolkit_name, pip_package_name, sample_tool.requirements.authorization)
|
footer = build_footer(toolkit_name, pip_package_name, sample_tool.requirements.authorization)
|
||||||
|
|
||||||
referenced_enums, tools_specs = build_tools_specs(tools, docs_section, enums)
|
referenced_enums, tools_specs = build_tools_specs(tools, docs_section, enums)
|
||||||
reference_mdx = build_reference_mdx(toolkit_name, referenced_enums) if referenced_enums else ""
|
reference_mdx = build_reference_mdx(toolkit_name, referenced_enums) if referenced_enums else ""
|
||||||
|
|
||||||
return reference_mdx, toolkit_page_template.format(
|
toolkit_mdx = toolkit_page_template.format(
|
||||||
header=header,
|
header=header,
|
||||||
table_of_contents=table_of_contents,
|
table_of_contents=table_of_contents,
|
||||||
tools_specs=tools_specs,
|
tools_specs=tools_specs,
|
||||||
|
reference_mdx=reference_mdx,
|
||||||
footer=footer,
|
footer=footer,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return toolkit_mdx.strip()
|
||||||
|
|
||||||
|
|
||||||
def build_reference_mdx(
|
def build_reference_mdx(
|
||||||
toolkit_name: str,
|
toolkit_name: str,
|
||||||
|
|
@ -327,7 +322,8 @@ def build_examples(
|
||||||
examples = []
|
examples = []
|
||||||
for tool in tools:
|
for tool in tools:
|
||||||
print_debug(f"Generating tool-call examples for {tool.name}")
|
print_debug(f"Generating tool-call examples for {tool.name}")
|
||||||
input_map = generate_tool_input_map(tool, openai_model)
|
interface_signature = build_tool_interface_signature(tool)
|
||||||
|
input_map = generate_tool_input_map(interface_signature, openai_model)
|
||||||
fully_qualified_name = tool.fully_qualified_name.split("@")[0]
|
fully_qualified_name = tool.fully_qualified_name.split("@")[0]
|
||||||
examples.append((
|
examples.append((
|
||||||
f"{pascal_to_snake_case(tool.name)}_example_call_tool.py",
|
f"{pascal_to_snake_case(tool.name)}_example_call_tool.py",
|
||||||
|
|
@ -375,8 +371,6 @@ def generate_toolkit_description(
|
||||||
tools: list[tuple[str, str]],
|
tools: list[tuple[str, str]],
|
||||||
openai_model: str,
|
openai_model: str,
|
||||||
) -> str:
|
) -> str:
|
||||||
response = openai.chat.completions.create(
|
|
||||||
model=openai_model,
|
|
||||||
messages = [
|
messages = [
|
||||||
{
|
{
|
||||||
"role": "system",
|
"role": "system",
|
||||||
|
|
@ -432,24 +426,17 @@ def generate_toolkit_description(
|
||||||
"Please generate a description for the toolkit."
|
"Please generate a description for the toolkit."
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
],
|
]
|
||||||
temperature=0.0,
|
|
||||||
max_tokens=2048,
|
|
||||||
)
|
|
||||||
|
|
||||||
response_str = cast(str, response.choices[0].message.content)
|
return request_openai_generation(model=openai_model, max_tokens=512, messages=messages)
|
||||||
return response_str.strip()
|
|
||||||
|
|
||||||
|
|
||||||
def generate_tool_input_map(
|
def generate_tool_input_map(
|
||||||
tool: ToolDefinition,
|
interface_signature: dict[str, Any],
|
||||||
openai_model: str,
|
openai_model: str,
|
||||||
retries: int = 0,
|
retries: int = 0,
|
||||||
max_retries: int = 3,
|
max_retries: int = 3,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
interface_signature = build_tool_interface_signature(tool)
|
|
||||||
response = openai.chat.completions.create(
|
|
||||||
model=openai_model,
|
|
||||||
messages = [
|
messages = [
|
||||||
{
|
{
|
||||||
"role": "system",
|
"role": "system",
|
||||||
|
|
@ -481,28 +468,34 @@ def generate_tool_input_map(
|
||||||
"role": "user",
|
"role": "user",
|
||||||
"content": (
|
"content": (
|
||||||
"Here is a tool interface:\n\n"
|
"Here is a tool interface:\n\n"
|
||||||
f"{interface_signature}\n\n"
|
f"{json.dumps(interface_signature, ensure_ascii=False)}\n\n"
|
||||||
"Please provide a sample input map as a JSON object."
|
"Please provide a sample input map as a JSON object."
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
],
|
]
|
||||||
temperature=0.0,
|
|
||||||
max_tokens=1024,
|
|
||||||
stop=["\n\n"],
|
|
||||||
)
|
|
||||||
|
|
||||||
response_str = cast(str, response.choices[0].message.content)
|
text = request_openai_generation(model=openai_model, max_tokens=512, messages=messages)
|
||||||
text = response_str.strip()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return cast(dict[str, Any], json.loads(text))
|
return cast(dict[str, Any], json.loads(text))
|
||||||
except json.JSONDecodeError:
|
except (json.JSONDecodeError, TypeError):
|
||||||
if retries < max_retries:
|
if retries < max_retries:
|
||||||
return generate_tool_input_map(tool, openai_model, retries + 1, max_retries)
|
return generate_tool_input_map(
|
||||||
raise ValueError(f"Failed to generate input map for tool {tool.name}: {text}")
|
interface_signature=interface_signature,
|
||||||
|
openai_model=openai_model,
|
||||||
|
retries=retries + 1,
|
||||||
|
max_retries=max_retries,
|
||||||
|
)
|
||||||
|
tool_name = interface_signature["tool_name"]
|
||||||
|
console.print(
|
||||||
|
f"Attention: {openai_model} failed to generate a valid inputs JSON for the tool '{tool_name}'. "
|
||||||
|
"Please check the Python & Javascript example scripts generated and enter a sample input manually.",
|
||||||
|
style="red",
|
||||||
|
)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def build_tool_interface_signature(tool: ToolDefinition) -> str:
|
def build_tool_interface_signature(tool: ToolDefinition) -> dict[str, Any]:
|
||||||
args = []
|
args = []
|
||||||
for arg in tool.input.parameters:
|
for arg in tool.input.parameters:
|
||||||
data: dict[str, Any] = {
|
data: dict[str, Any] = {
|
||||||
|
|
@ -519,8 +512,45 @@ def build_tool_interface_signature(tool: ToolDefinition) -> str:
|
||||||
|
|
||||||
args.append(data)
|
args.append(data)
|
||||||
|
|
||||||
return json.dumps({
|
return {
|
||||||
"tool_name": tool.name,
|
"tool_name": tool.name,
|
||||||
"tool_description": tool.description,
|
"tool_description": tool.description,
|
||||||
"tool_args": args,
|
"tool_args": args,
|
||||||
})
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def request_openai_generation(
|
||||||
|
model: str,
|
||||||
|
max_tokens: int,
|
||||||
|
messages: list[dict[str, Any]],
|
||||||
|
) -> str:
|
||||||
|
if model.startswith("gpt-5"):
|
||||||
|
response = openai.responses.create(
|
||||||
|
model=model,
|
||||||
|
input=messages,
|
||||||
|
max_output_tokens=max_tokens,
|
||||||
|
reasoning={
|
||||||
|
"effort": "minimal",
|
||||||
|
},
|
||||||
|
text={
|
||||||
|
"verbosity": "low",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
response_str = cast(str, response.output_text)
|
||||||
|
|
||||||
|
elif model.startswith("gpt-4o"):
|
||||||
|
response = openai.chat.completions.create(
|
||||||
|
model=model,
|
||||||
|
messages=messages,
|
||||||
|
temperature=0.0,
|
||||||
|
max_completion_tokens=max_tokens,
|
||||||
|
stop=["\n\n"],
|
||||||
|
)
|
||||||
|
response_str = cast(str, response.choices[0].message.content)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"Unsupported OpenAI model: {model}. Choose a model from the 'gpt-4o' or 'gpt-5' series."
|
||||||
|
)
|
||||||
|
|
||||||
|
return response_str.strip()
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ TOOLKIT_PAGE = """{header}
|
||||||
{table_of_contents}
|
{table_of_contents}
|
||||||
|
|
||||||
{tools_specs}
|
{tools_specs}
|
||||||
|
{reference_mdx}
|
||||||
{footer}
|
{footer}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -143,9 +143,9 @@ response = client.tools.execute(
|
||||||
print(json.dumps(response.output.value, indent=2))
|
print(json.dumps(response.output.value, indent=2))
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ENUM_MDX = """# {toolkit_name} Reference
|
ENUM_MDX = """## Reference
|
||||||
|
|
||||||
Below is a reference of enumerations used by some tools in the {toolkit_name} toolkit:
|
Below is a reference of enumerations used by some of the tools in the {toolkit_name} toolkit:
|
||||||
|
|
||||||
{enum_items}
|
{enum_items}
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue