agentcode-cli 1.1.0__py3-none-any.whl → 1.2.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
agent.py CHANGED
@@ -18,6 +18,8 @@ import litellm
18
18
  from rich.console import Console
19
19
  from rich.markdown import Markdown
20
20
 
21
+ from xml_tool_parser import looks_like_xml_tool_call, parse_xml_tool_calls, strip_think
22
+
21
23
  from tools import TOOL_DEFINITIONS, execute_tool
22
24
  from router import ModelRouter, display_routing_decision
23
25
  from mcp_client import MCPManager
@@ -346,6 +348,18 @@ def run_agent_loop(
346
348
  cost,
347
349
  )
348
350
 
351
+ # Open-weight fine-tunes (e.g. Vigp17/agentcode-27b) may emit tool
352
+ # calls as inline XML in the text response. Detect and convert before
353
+ # treating this as a pure text turn.
354
+ if not tool_calls_accum and looks_like_xml_tool_call(full_text):
355
+ cleaned, xml_calls = parse_xml_tool_calls(full_text)
356
+ if xml_calls:
357
+ full_text = cleaned
358
+ for i, tc in enumerate(xml_calls):
359
+ tool_calls_accum[i] = tc
360
+ elif not tool_calls_accum:
361
+ full_text = strip_think(full_text)
362
+
349
363
  # Pure text response — done
350
364
  if not tool_calls_accum:
351
365
  conversation.messages.append({"role": "assistant", "content": full_text})
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentcode-cli
3
- Version: 1.1.0
3
+ Version: 1.2.2
4
4
  Summary: An open, multi-model agentic coding CLI — inspired by Claude Code
5
5
  Author: Vignesh Pai
6
6
  License: MIT License
@@ -0,0 +1,14 @@
1
+ agent.py,sha256=lBZ8vsduMGt30E7VsPKbByA96Hum8B5pEIE_B7-QlrY,17784
2
+ cli.py,sha256=FhaY-UCKQdmAcKeLw6Cvd2kDowh6yRlvmaKGfGECk80,25074
3
+ mcp_client.py,sha256=2PviTqJtXM4UC_fsYLbAOAfWJvayWy7Q8VOQIIsDiqQ,6710
4
+ router.py,sha256=IjomIOaLVmGHQG9rNgc3xNpGhr82rCnn5h8X-QvZhRU,11573
5
+ server.py,sha256=KZhq4rYsKlRBth1g2fkqYHdCelJR4DD4-ZugC4jAgtw,10334
6
+ settings.py,sha256=Qjc3tiVbT1cqIrnQW6m2UG8Xsvqsxl9qXPMTueqwn50,6903
7
+ tools.py,sha256=MBYy0OeSIZjqyGOezaMvZ6AMW3WRDESU-2u48sifd0Q,24741
8
+ xml_tool_parser.py,sha256=T62DigRj8VCH30dq5AnxV8hsJXf0f2vyubg5F4nzDDk,3138
9
+ agentcode_cli-1.2.2.dist-info/licenses/LICENSE,sha256=BqTzyKKaSaVQoumXzhYCj1UgOSPCgvn-sxV6BIuT558,1068
10
+ agentcode_cli-1.2.2.dist-info/METADATA,sha256=oRCW6r2goUhjL-nrryIi0i0Su-2A45r_128KbameN3Y,12709
11
+ agentcode_cli-1.2.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
12
+ agentcode_cli-1.2.2.dist-info/entry_points.txt,sha256=xP_zeySufuVhL5v10_EqCooKRSxGYB8QuRVNUj4_m1E,39
13
+ agentcode_cli-1.2.2.dist-info/top_level.txt,sha256=684sAEdroiDabB0I7Zud0vYWXcDbCl2WK0LjTQzxiUk,66
14
+ agentcode_cli-1.2.2.dist-info/RECORD,,
@@ -5,3 +5,4 @@ router
5
5
  server
6
6
  settings
7
7
  tools
8
+ xml_tool_parser
cli.py CHANGED
@@ -151,6 +151,8 @@ def handle_slash_command(
151
151
  config.model = arg
152
152
  if config.router:
153
153
  config.router.enabled = False
154
+ config.router.default_model = arg
155
+ config.router.provider = config.router.detect_provider(arg)
154
156
  console.print(f"[success]✓ Switched to model: {arg} (routing disabled)[/success]")
155
157
  else:
156
158
  console.print(f"[info]Current model: {config.model}[/info]")
router.py CHANGED
@@ -47,11 +47,20 @@ GEMINI_TIERS = [
47
47
  ModelTier("gemini/gemini-2.5-pro", "heavy", "Gemini 2.5 Pro", 1.25, 10.00),
48
48
  ]
49
49
 
50
+ # Azure deployment names are user-defined, so tiers use common defaults.
51
+ # Users can override via settings.json model.light/medium/heavy.
52
+ AZURE_TIERS = [
53
+ ModelTier("azure/gpt-4o-mini", "light", "Azure GPT-4o Mini", 0.15, 0.60),
54
+ ModelTier("azure/gpt-4o", "medium", "Azure GPT-4o", 2.50, 10.00),
55
+ ModelTier("azure/gpt-4o", "heavy", "Azure GPT-4o", 2.50, 10.00),
56
+ ]
57
+
50
58
  # Provider configs keyed by prefix
51
59
  PROVIDER_TIERS = {
52
60
  "anthropic": ANTHROPIC_TIERS,
53
61
  "openai": OPENAI_TIERS,
54
62
  "gemini": GEMINI_TIERS,
63
+ "azure": AZURE_TIERS,
55
64
  }
56
65
 
57
66
 
@@ -203,7 +212,9 @@ class ModelRouter:
203
212
  def detect_provider(self, model_string: str) -> str:
204
213
  """Detect provider from a model string."""
205
214
  m = model_string.lower()
206
- if "claude" in m or "anthropic" in m:
215
+ if m.startswith("azure/"):
216
+ return "azure"
217
+ elif "claude" in m or "anthropic" in m:
207
218
  return "anthropic"
208
219
  elif "gpt" in m or "openai" in m or "o1" in m or "o3" in m:
209
220
  return "openai"
server.py CHANGED
@@ -12,6 +12,7 @@ from agent import (
12
12
  build_system_prompt, load_project_config, load_hooks,
13
13
  _run_subagents, _run_hook, _is_denied, _get_permission,
14
14
  )
15
+ from xml_tool_parser import looks_like_xml_tool_call, parse_xml_tool_calls, strip_think
15
16
 
16
17
 
17
18
  # ── Wire protocol ─────────────────────────────────────────────────────────────
@@ -106,6 +107,18 @@ def _server_turn(
106
107
  if tc.function.arguments:
107
108
  tool_calls_accum[idx]["arguments"] += tc.function.arguments
108
109
 
110
+ # Some open-weight fine-tunes (e.g. Vigp17/agentcode-27b) emit tool
111
+ # calls as inline XML in the text response instead of using litellm's
112
+ # structured tool_calls field. Detect and convert.
113
+ if not tool_calls_accum and looks_like_xml_tool_call(full_text):
114
+ cleaned, xml_calls = parse_xml_tool_calls(full_text)
115
+ if xml_calls:
116
+ full_text = cleaned
117
+ for i, tc in enumerate(xml_calls):
118
+ tool_calls_accum[i] = tc
119
+ elif not tool_calls_accum:
120
+ full_text = strip_think(full_text)
121
+
109
122
  if router and usage:
110
123
  try:
111
124
  cost = litellm.completion_cost(
xml_tool_parser.py ADDED
@@ -0,0 +1,87 @@
1
+ """
2
+ Parse Hermes/Qwen-3-style XML tool calls into OpenAI-compatible tool_calls.
3
+
4
+ Some fine-tuned open-weight models (e.g. Vigp17/agentcode-27b) emit tool calls
5
+ as inline XML in the assistant's text response instead of using the structured
6
+ `tool_calls` field that litellm/OpenAI clients expect. This module detects that
7
+ shape and converts it.
8
+
9
+ Input shape we handle:
10
+
11
+ <think>...optional reasoning...</think>
12
+
13
+ <tool_call>
14
+ <function=tool_name>
15
+ <parameter=key1>value1</parameter>
16
+ <parameter=key2>value2</parameter>
17
+ </function>
18
+ </tool_call>
19
+
20
+ Output: the same payload as litellm's structured tool_calls — a list of
21
+ {"id", "name", "arguments"} dicts plus the cleaned text (think blocks stripped,
22
+ tool_call blocks removed).
23
+ """
24
+
25
+ import json
26
+ import re
27
+ import uuid
28
+
29
+
30
+ _THINK_RE = re.compile(r"<think>.*?</think>\s*", re.DOTALL)
31
+ # Qwen 3 / similar templates often consume the opening <think> tag during
32
+ # prompt construction but leave the </think>. In that case everything from
33
+ # the start of the response up to and including the first </think> is
34
+ # reasoning that should be stripped.
35
+ _OPEN_THINK_MISSING_RE = re.compile(r"\A.*?</think>\s*", re.DOTALL)
36
+ _TOOL_CALL_RE = re.compile(r"<tool_call>(.*?)</tool_call>", re.DOTALL)
37
+ _FUNCTION_RE = re.compile(r"<function=([^>]+)>(.*?)</function>", re.DOTALL)
38
+ _PARAMETER_RE = re.compile(r"<parameter=([^>]+)>(.*?)</parameter>", re.DOTALL)
39
+
40
+
41
+ def looks_like_xml_tool_call(text: str) -> bool:
42
+ """Cheap check before doing full regex parsing."""
43
+ return "<tool_call>" in text and "<function=" in text
44
+
45
+
46
+ def strip_think(text: str) -> str:
47
+ """Remove <think>...</think> blocks so users don't see chain-of-thought.
48
+
49
+ Also handles the chat-template-ate-the-opening-tag case: if </think>
50
+ appears but no matching <think>, strip everything up to and including it.
51
+ """
52
+ text = _THINK_RE.sub("", text)
53
+ if "</think>" in text and "<think>" not in text:
54
+ text = _OPEN_THINK_MISSING_RE.sub("", text)
55
+ return text
56
+
57
+
58
+ def parse_xml_tool_calls(text: str) -> tuple[str, list[dict]]:
59
+ """
60
+ Extract XML tool calls from text and return (cleaned_text, tool_calls).
61
+
62
+ cleaned_text has the <think> and <tool_call> blocks removed so it can be
63
+ streamed to the user as a normal assistant reply.
64
+
65
+ tool_calls is a list of dicts shaped like litellm's accumulator:
66
+ {"id": "call_xxx", "name": "tool_name", "arguments": '{"key": "value"}'}
67
+ """
68
+ cleaned = strip_think(text)
69
+ tool_calls: list[dict] = []
70
+
71
+ for tc_match in _TOOL_CALL_RE.finditer(cleaned):
72
+ body = tc_match.group(1)
73
+ for fn_match in _FUNCTION_RE.finditer(body):
74
+ name = fn_match.group(1).strip()
75
+ params_body = fn_match.group(2)
76
+ args = {
77
+ p.group(1).strip(): p.group(2).strip()
78
+ for p in _PARAMETER_RE.finditer(params_body)
79
+ }
80
+ tool_calls.append({
81
+ "id": f"call_{uuid.uuid4().hex[:8]}",
82
+ "name": name,
83
+ "arguments": json.dumps(args),
84
+ })
85
+
86
+ cleaned = _TOOL_CALL_RE.sub("", cleaned).strip()
87
+ return cleaned, tool_calls
@@ -1,13 +0,0 @@
1
- agent.py,sha256=zcYfTAarYhj1hdLc9DH5sWs6Qqqp1VpmrHGJfHSJZIQ,17118
2
- cli.py,sha256=lzZzJkfAJ7nzmTZqIkRnVfJg1SsfuhMwge9J7iX9xM4,24948
3
- mcp_client.py,sha256=2PviTqJtXM4UC_fsYLbAOAfWJvayWy7Q8VOQIIsDiqQ,6710
4
- router.py,sha256=kjPpF1js6mA7FN2x_NleyN-BLeVzRAy-Q0wbhjelQzM,11109
5
- server.py,sha256=w7EXPCsXHiSqfIXep3VIcS66t_ZY02ulF2Yn6e7N5TY,9654
6
- settings.py,sha256=Qjc3tiVbT1cqIrnQW6m2UG8Xsvqsxl9qXPMTueqwn50,6903
7
- tools.py,sha256=MBYy0OeSIZjqyGOezaMvZ6AMW3WRDESU-2u48sifd0Q,24741
8
- agentcode_cli-1.1.0.dist-info/licenses/LICENSE,sha256=BqTzyKKaSaVQoumXzhYCj1UgOSPCgvn-sxV6BIuT558,1068
9
- agentcode_cli-1.1.0.dist-info/METADATA,sha256=u7sKWqB7yfo66ODU2HGTAtEudTIR8dw1QKxkoPaE6AM,12709
10
- agentcode_cli-1.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
11
- agentcode_cli-1.1.0.dist-info/entry_points.txt,sha256=xP_zeySufuVhL5v10_EqCooKRSxGYB8QuRVNUj4_m1E,39
12
- agentcode_cli-1.1.0.dist-info/top_level.txt,sha256=PQseaNK25xxImV2WLHRHRIkipStqhLdVWYmrccM5ln0,50
13
- agentcode_cli-1.1.0.dist-info/RECORD,,