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
142 changes: 142 additions & 0 deletions examples/agent_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
"""
Minimal agent client for the InfiniLM inference server.

Demonstrates the complete tool-call loop over the OpenAI-compatible
``/v1/chat/completions`` endpoint:

request (with tools) -> model returns tool_calls
-> client executes the tools locally
-> results are appended as assistant/tool messages
-> repeat until the model answers in plain text

This is the same loop an agent framework (e.g. Claude Code) drives; it is
kept dependency-free and with a short system prompt on purpose, so the loop
can be validated on small models where huge agent prompts degrade tool use.

Usage (server started with ``--tool-call-parser llama31`` or
``--tool-call-parser glm4-9b-0414`` etc.):

python examples/agent_client.py --url http://127.0.0.1:8000 \
--model GLM-4-9B-0414 "北京天气怎么样?顺便看看当前目录有什么文件"
"""

import argparse
import json
import os
import urllib.request

TOOLS = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather for a city.",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string", "description": "City name"}},
"required": ["city"],
},
},
},
{
"type": "function",
"function": {
"name": "list_dir",
"description": "List files and directories at a path.",
"parameters": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "Directory path"}
},
"required": ["path"],
},
},
},
]


def execute_tool(name: str, arguments: dict) -> str:
"""Execute a tool locally and return its result as a string."""
if name == "get_weather":
city = arguments.get("city", "")
return json.dumps(
{"city": city, "weather": "晴", "temperature": "26度"}, ensure_ascii=False
)
if name == "list_dir":
path = arguments.get("path", ".")
try:
entries = sorted(os.listdir(path))
except OSError as e:
return f"error: {e}"
return "\n".join(entries) if entries else "(empty directory)"
return f"error: unknown tool {name}"


def chat(url: str, payload: dict) -> dict:
request = urllib.request.Request(
f"{url}/v1/chat/completions",
data=json.dumps(payload).encode(),
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(request, timeout=600) as response:
return json.loads(response.read())


def run(url: str, model: str, question: str, max_turns: int = 6):
messages = [
{
"role": "system",
"content": "You are a helpful assistant. Use the available tools "
"to answer the user's question.",
},
{"role": "user", "content": question},
]

for turn in range(max_turns):
response = chat(
url,
{
"model": model,
"messages": messages,
"tools": TOOLS,
"max_tokens": 1024,
"stream": False,
},
)
choice = response["choices"][0]
message = choice["message"]
tool_calls = message.get("tool_calls") or []

if not tool_calls:
print(f"\n[turn {turn}] assistant: {message.get('content', '')}")
return

# Record the assistant tool-call turn, execute, feed results back.
messages.append(message)
for call in tool_calls:
name = call["function"]["name"]
try:
arguments = json.loads(call["function"]["arguments"] or "{}")
except json.JSONDecodeError:
arguments = {}
result = execute_tool(name, arguments)
print(f"[turn {turn}] tool_call: {name}({arguments}) -> {result[:80]}")
messages.append(
{"role": "tool", "tool_call_id": call["id"], "content": result}
)

print("\n[max turns reached without a final answer]")


def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--url", default="http://127.0.0.1:8000")
parser.add_argument("--model", default="GLM-4-9B-0414")
parser.add_argument("--max-turns", type=int, default=6)
parser.add_argument("question", nargs="+")
args = parser.parse_args()
run(args.url, args.model, " ".join(args.question), args.max_turns)


if __name__ == "__main__":
main()
7 changes: 6 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ name = "InfiniLM"
version = "0.1.0"
description = "InfiniLM model implementations"
readme = "README.md"
dependencies = []
dependencies = [
# Incremental (streaming) parsing of incomplete tool-call arguments.
# Without it the fallback decoder can only parse fully-formed JSON, so
# streamed tool-call arguments are held back until the object is complete.
"partial-json-parser>=0.2.1.1",
]
requires-python = ">=3.10"
classifiers = [
"Programming Language :: Python :: 3",
Expand Down
30 changes: 30 additions & 0 deletions python/infinilm/agents/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""
Agent support for InfiniLM: tool calls and reasoning parsing.
Supports GLM-4, Llama-3.1+, and Qwen3 tool call formats.
"""

from .function_call_parser import FunctionCallParser
from .message_adapter import adapt_messages
from .protocol import DeltaMessage, Function, Tool, ToolChoice
from .reasoning_parser import ReasoningParser
from .stream_parser import (
AgentDelta,
AgentStreamParser,
parse_full_response,
)
from .types import StreamingParseResult, ToolCallItem

__all__ = [
"ToolCallItem",
"StreamingParseResult",
"Tool",
"ToolChoice",
"Function",
"DeltaMessage",
"FunctionCallParser",
"ReasoningParser",
"AgentDelta",
"AgentStreamParser",
"parse_full_response",
"adapt_messages",
]
Loading
Loading