regcode 0.12.0__tar.gz → 0.12.1__tar.gz
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.
- {regcode-0.12.0 → regcode-0.12.1}/GOALS.md +51 -1
- {regcode-0.12.0 → regcode-0.12.1}/PKG-INFO +1 -1
- {regcode-0.12.0 → regcode-0.12.1}/pyproject.toml +1 -1
- {regcode-0.12.0 → regcode-0.12.1}/regcode/__init__.py +1 -1
- {regcode-0.12.0 → regcode-0.12.1}/regcode/agents_backend.py +86 -14
- regcode-0.12.1/regcode/async_utils.py +58 -0
- {regcode-0.12.0 → regcode-0.12.1}/regcode/main.py +54 -18
- {regcode-0.12.0 → regcode-0.12.1}/regcode/monty_sandbox.py +13 -11
- {regcode-0.12.0 → regcode-0.12.1}/regcode/tools/web_crawl.py +6 -3
- {regcode-0.12.0 → regcode-0.12.1}/tests/test_clipboard.py +9 -1
- {regcode-0.12.0 → regcode-0.12.1}/tests/test_main.py +8 -1
- {regcode-0.12.0 → regcode-0.12.1}/tests/test_task_regression.py +292 -0
- {regcode-0.12.0 → regcode-0.12.1}/uv.lock +36 -36
- {regcode-0.12.0 → regcode-0.12.1}/.gitignore +0 -0
- {regcode-0.12.0 → regcode-0.12.1}/.gitlab-ci.yml +0 -0
- {regcode-0.12.0 → regcode-0.12.1}/.python-version +0 -0
- {regcode-0.12.0 → regcode-0.12.1}/.qwen/settings.json +0 -0
- {regcode-0.12.0 → regcode-0.12.1}/AGENTS.md +0 -0
- {regcode-0.12.0 → regcode-0.12.1}/README.md +0 -0
- {regcode-0.12.0 → regcode-0.12.1}/config.yaml.template +0 -0
- {regcode-0.12.0 → regcode-0.12.1}/main.py +0 -0
- {regcode-0.12.0 → regcode-0.12.1}/regcode/cli.py +0 -0
- {regcode-0.12.0 → regcode-0.12.1}/regcode/clipboard.py +0 -0
- {regcode-0.12.0 → regcode-0.12.1}/regcode/config.py +0 -0
- {regcode-0.12.0 → regcode-0.12.1}/regcode/conversation_manager.py +0 -0
- {regcode-0.12.0 → regcode-0.12.1}/regcode/dependencies.py +0 -0
- {regcode-0.12.0 → regcode-0.12.1}/regcode/mcp_client.py +0 -0
- {regcode-0.12.0 → regcode-0.12.1}/regcode/paste.py +0 -0
- {regcode-0.12.0 → regcode-0.12.1}/regcode/permissions.py +0 -0
- {regcode-0.12.0 → regcode-0.12.1}/regcode/sandbox.py +0 -0
- {regcode-0.12.0 → regcode-0.12.1}/regcode/tools/__init__.py +0 -0
- {regcode-0.12.0 → regcode-0.12.1}/regcode/tools/base.py +0 -0
- {regcode-0.12.0 → regcode-0.12.1}/regcode/tools/builtins.py +0 -0
- {regcode-0.12.0 → regcode-0.12.1}/regcode/tools/registry.py +0 -0
- {regcode-0.12.0 → regcode-0.12.1}/regcode/tools/review_notes.py +0 -0
- {regcode-0.12.0 → regcode-0.12.1}/regcode/tools/web_search.py +0 -0
- {regcode-0.12.0 → regcode-0.12.1}/regcode/tracing.py +0 -0
- {regcode-0.12.0 → regcode-0.12.1}/regcode/tui.py +0 -0
- {regcode-0.12.0 → regcode-0.12.1}/tasks.md +0 -0
- {regcode-0.12.0 → regcode-0.12.1}/tests/__init__.py +0 -0
- {regcode-0.12.0 → regcode-0.12.1}/tests/conftest.py +0 -0
- {regcode-0.12.0 → regcode-0.12.1}/tests/llm_fakes.py +0 -0
- {regcode-0.12.0 → regcode-0.12.1}/tests/test_api.py +0 -0
- {regcode-0.12.0 → regcode-0.12.1}/tests/test_cli.py +0 -0
- {regcode-0.12.0 → regcode-0.12.1}/tests/test_config.py +0 -0
- {regcode-0.12.0 → regcode-0.12.1}/tests/test_context_manager.py +0 -0
- {regcode-0.12.0 → regcode-0.12.1}/tests/test_dependencies.py +0 -0
- {regcode-0.12.0 → regcode-0.12.1}/tests/test_mcp.py +0 -0
- {regcode-0.12.0 → regcode-0.12.1}/tests/test_mcp_status.py +0 -0
- {regcode-0.12.0 → regcode-0.12.1}/tests/test_monty_sandbox.py +0 -0
- {regcode-0.12.0 → regcode-0.12.1}/tests/test_osv_cve.py +0 -0
- {regcode-0.12.0 → regcode-0.12.1}/tests/test_provider_extra.py +0 -0
- {regcode-0.12.0 → regcode-0.12.1}/tests/test_review_notes.py +0 -0
- {regcode-0.12.0 → regcode-0.12.1}/tests/test_web_search.py +0 -0
|
@@ -643,5 +643,55 @@ for the per-task mapping to tests.
|
|
|
643
643
|
restoring it, so later tests received a second `AgentStatus` class and enum identity
|
|
644
644
|
comparisons failed purely on execution order.
|
|
645
645
|
|
|
646
|
-
11. ✅ **
|
|
646
|
+
11. ✅ **285 tests pass** (up from 231), `uv run ruff check` clean.
|
|
647
|
+
|
|
648
|
+
## Fixed (2026-10-31 — CLI broke on the first real conversation)
|
|
649
|
+
|
|
650
|
+
The rewrite passed every test but broke `regcode chat` as soon as the model said
|
|
651
|
+
anything before calling a tool:
|
|
652
|
+
|
|
653
|
+
```
|
|
654
|
+
[ERROR] Agent error! litellm.BadRequestError: Hosted_vllmException - {"error":{"message":
|
|
655
|
+
"3 validation errors for ValidatorIterator\n0.ChatCompletionContentPartTextParam.type\n
|
|
656
|
+
Input should be 'text' [type=literal_error, input_value='output_text'...
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
Three separate regressions, all invisible to the existing suite because **every test
|
|
660
|
+
made the model return *either* text *or* a tool call** — so the second turn, the one
|
|
661
|
+
where the Runner replays the assistant's own output, never executed.
|
|
662
|
+
|
|
663
|
+
1. ✅ **Responses-API items were leaking into chat-completions requests** — the Runner
|
|
664
|
+
hands the assistant's output back as `{"type": "message", "content": [{"type":
|
|
665
|
+
"output_text", ...}]}`, `function_call` and `function_call_output`. `_prepare_messages`
|
|
666
|
+
copied those dicts straight into `litellm.completion(messages=...)`, which the provider
|
|
667
|
+
rejects. It now runs them through `Converter.items_to_messages(items, model=...)`, the
|
|
668
|
+
same translation the SDK's own `LitellmModel` uses — which also merges an assistant's
|
|
669
|
+
text with the tool calls it asked for into a single turn.
|
|
670
|
+
|
|
671
|
+
2. ✅ **Replayed history was structurally invalid** — `_update_history_from_sdk_result`
|
|
672
|
+
stored a `"Tool call: x"` label next to a `role: "tool"` result. A tool result is only
|
|
673
|
+
legal when an assistant turn carrying the matching `tool_calls` precedes it, so the
|
|
674
|
+
second `chat()` call would have been rejected by strict providers (vLLM among them).
|
|
675
|
+
History now stores the real `tool_calls` array (id, name, arguments). `ToolCallItem.raw_item`
|
|
676
|
+
is a `ResponseFunctionToolCall` **object** when the Runner builds it and a dict elsewhere;
|
|
677
|
+
reading it only as a dict silently produced `arguments: "{}"`, so a replayed turn claimed
|
|
678
|
+
the model had called a tool with no arguments. `_item_field()` handles both.
|
|
679
|
+
|
|
680
|
+
3. ✅ **`python_code`, `run_script` and `web_crawl` were dead** — moving tool execution
|
|
681
|
+
under the Runner put them *inside* an event loop, where `asyncio.run()` raises
|
|
682
|
+
`cannot be called from a running event loop`. Every `python_code` call returned
|
|
683
|
+
`SANDBOX_ERROR: ...` and the model just got an error string back. Added
|
|
684
|
+
`regcode/async_utils.py::run_coroutine_blocking()`, which runs the coroutine on a
|
|
685
|
+
dedicated thread when a loop is already active.
|
|
686
|
+
|
|
687
|
+
Also: `chat(messages=...)` is public API (`code_review`, GitKnecht, saved transcripts)
|
|
688
|
+
and accepts plain chat-shaped dicts, which `Converter.items_to_messages` rejects outright.
|
|
689
|
+
`_expand_input_items()` normalises `role: "tool"` and assistant `tool_calls` into Responses
|
|
690
|
+
items before conversion.
|
|
691
|
+
|
|
692
|
+
**Process lesson**: the new tests were written against the shapes the *tests* invented.
|
|
693
|
+
`tests/test_task_regression.py` now pins the message shape of every turn handed to litellm
|
|
694
|
+
(`_assert_chat_completions_shape`), covers text-plus-tool-call turns, a second `chat()`
|
|
695
|
+
call, and tool execution under a running loop. Each was mutation-checked — revert the fix,
|
|
696
|
+
watch it go red.
|
|
647
697
|
|
|
@@ -103,6 +103,69 @@ def _base_response(model: str) -> Response:
|
|
|
103
103
|
)
|
|
104
104
|
|
|
105
105
|
|
|
106
|
+
def _output_text_part(text: str) -> dict[str, Any]:
|
|
107
|
+
"""Build a Responses ``output_text`` content part."""
|
|
108
|
+
return {"annotations": [], "text": text, "type": "output_text"}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _expand_input_items(items: list[Any]) -> list[Any]:
|
|
112
|
+
"""Normalise chat-completions-shaped dicts into Responses input items.
|
|
113
|
+
|
|
114
|
+
``chat(messages=...)`` is public API and callers (``code_review``,
|
|
115
|
+
GitKnecht, saved transcripts) pass plain ``{"role": ..., "content": ...}``
|
|
116
|
+
dicts, including the ``role: "tool"`` entries older versions stored in
|
|
117
|
+
``Agent.history``. ``Converter.items_to_messages`` only understands
|
|
118
|
+
Responses items and raises ``UserError`` on those, which would kill the
|
|
119
|
+
whole run. Plain ``{role, content}`` dicts pass through untouched.
|
|
120
|
+
"""
|
|
121
|
+
expanded: list[Any] = []
|
|
122
|
+
for item in items:
|
|
123
|
+
if not isinstance(item, dict):
|
|
124
|
+
expanded.append(item)
|
|
125
|
+
continue
|
|
126
|
+
|
|
127
|
+
role = item.get("role")
|
|
128
|
+
tool_calls = item.get("tool_calls")
|
|
129
|
+
|
|
130
|
+
if role == "tool":
|
|
131
|
+
expanded.append(
|
|
132
|
+
{
|
|
133
|
+
"type": "function_call_output",
|
|
134
|
+
"call_id": item.get("tool_call_id") or "call_unknown",
|
|
135
|
+
"output": item.get("content") or "",
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
if tool_calls:
|
|
141
|
+
# A chat-shaped assistant turn bundles the text and the calls;
|
|
142
|
+
# Responses keeps them as separate items, calls first.
|
|
143
|
+
for call in tool_calls:
|
|
144
|
+
function = call.get("function") or {}
|
|
145
|
+
expanded.append(
|
|
146
|
+
{
|
|
147
|
+
"type": "function_call",
|
|
148
|
+
"call_id": call.get("id") or "call_unknown",
|
|
149
|
+
"name": function.get("name") or "",
|
|
150
|
+
"arguments": function.get("arguments") or "{}",
|
|
151
|
+
}
|
|
152
|
+
)
|
|
153
|
+
content = item.get("content")
|
|
154
|
+
if isinstance(content, str) and content:
|
|
155
|
+
expanded.append(
|
|
156
|
+
{
|
|
157
|
+
"type": "message",
|
|
158
|
+
"role": "assistant",
|
|
159
|
+
"status": "completed",
|
|
160
|
+
"content": [_output_text_part(content)],
|
|
161
|
+
}
|
|
162
|
+
)
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
expanded.append(item)
|
|
166
|
+
return expanded
|
|
167
|
+
|
|
168
|
+
|
|
106
169
|
class RegCodeModel(Model):
|
|
107
170
|
"""OpenAI Agents SDK ``Model`` backed by regcode's LiteLLM config.
|
|
108
171
|
|
|
@@ -279,7 +342,22 @@ class RegCodeModel(Model):
|
|
|
279
342
|
input: str | list[TResponseInputItem],
|
|
280
343
|
system_instructions: str | None,
|
|
281
344
|
) -> list[dict[str, Any]]:
|
|
282
|
-
"""Convert SDK input items into LiteLLM
|
|
345
|
+
"""Convert SDK input items into LiteLLM chat-completions messages.
|
|
346
|
+
|
|
347
|
+
The Runner hands back the assistant's own output as **Responses API**
|
|
348
|
+
items (``{"type": "message", "content": [{"type": "output_text", ...}]}``,
|
|
349
|
+
``function_call``, ``function_call_output``). Those must be translated
|
|
350
|
+
before they reach litellm: copying them through verbatim made the
|
|
351
|
+
second turn of every tool-using conversation fail with
|
|
352
|
+
|
|
353
|
+
3 validation errors for ValidatorIterator
|
|
354
|
+
.0.ChatCompletionContentPartTextParam.type
|
|
355
|
+
Input should be 'text' [input_value='output_text']
|
|
356
|
+
|
|
357
|
+
which surfaced in the CLI as an ``Agent error!`` the moment the model
|
|
358
|
+
said anything before calling a tool. ``Converter.items_to_messages`` is
|
|
359
|
+
the same translation the SDK's own ``LitellmModel`` uses, and it also
|
|
360
|
+
merges an assistant's text with the tool calls it asked for.
|
|
283
361
|
|
|
284
362
|
The system turn is managed **by role**, never by position: an existing
|
|
285
363
|
system message is replaced in place, and a caller list that starts with
|
|
@@ -290,19 +368,13 @@ class RegCodeModel(Model):
|
|
|
290
368
|
"""
|
|
291
369
|
items: list[Any] = [{"role": "user", "content": input}] if isinstance(
|
|
292
370
|
input, str
|
|
293
|
-
) else list(input)
|
|
294
|
-
|
|
295
|
-
messages: list[dict[str, Any]] =
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
messages.append(
|
|
301
|
-
{
|
|
302
|
-
"role": item.role,
|
|
303
|
-
"content": getattr(item, "content", ""),
|
|
304
|
-
}
|
|
305
|
-
)
|
|
371
|
+
) else _expand_input_items(list(input))
|
|
372
|
+
|
|
373
|
+
messages: list[dict[str, Any]] = list(
|
|
374
|
+
Converter.items_to_messages(
|
|
375
|
+
items, model=self.config.provider.model
|
|
376
|
+
)
|
|
377
|
+
)
|
|
306
378
|
|
|
307
379
|
replacement = self._system_text(system_instructions)
|
|
308
380
|
if replacement is None:
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Run async code from synchronous tool implementations.
|
|
2
|
+
|
|
3
|
+
Tools are invoked by the OpenAI Agents SDK ``Runner``, which drives the whole
|
|
4
|
+
conversation inside an event loop. A synchronous tool that calls
|
|
5
|
+
``asyncio.run()`` therefore dies with::
|
|
6
|
+
|
|
7
|
+
RuntimeError: asyncio.run() cannot be called from a running event loop
|
|
8
|
+
|
|
9
|
+
That is exactly what silently broke ``python_code``, ``run_script`` (both via
|
|
10
|
+
``MontySandbox``) and ``web_crawl`` once tool execution moved under the Runner.
|
|
11
|
+
The fix has to live in one place, because every tool that needs an event loop
|
|
12
|
+
has the same requirement.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
import threading
|
|
19
|
+
from collections.abc import Callable
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def run_coroutine_blocking(factory: Callable[[], Any]) -> Any:
|
|
24
|
+
"""Execute a coroutine to completion from sync code, loop present or not.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
factory: Zero-argument callable returning a fresh coroutine. It is
|
|
28
|
+
called lazily so the coroutine is always created on the thread that
|
|
29
|
+
will run it.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
The coroutine's result.
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
BaseException: Whatever the coroutine raised, re-raised on the calling
|
|
36
|
+
thread.
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
asyncio.get_running_loop()
|
|
40
|
+
except RuntimeError:
|
|
41
|
+
# No loop on this thread — the plain, cheap path.
|
|
42
|
+
return asyncio.run(factory())
|
|
43
|
+
|
|
44
|
+
box: dict[str, Any] = {}
|
|
45
|
+
|
|
46
|
+
def _worker() -> None:
|
|
47
|
+
try:
|
|
48
|
+
box["value"] = asyncio.run(factory())
|
|
49
|
+
except BaseException as exc: # re-raised on the calling thread
|
|
50
|
+
box["error"] = exc
|
|
51
|
+
|
|
52
|
+
# A dedicated thread gets its own loop, leaving the Runner's loop alone.
|
|
53
|
+
thread = threading.Thread(target=_worker, daemon=True)
|
|
54
|
+
thread.start()
|
|
55
|
+
thread.join()
|
|
56
|
+
if "error" in box:
|
|
57
|
+
raise box["error"]
|
|
58
|
+
return box.get("value")
|
|
@@ -761,11 +761,18 @@ class Agent:
|
|
|
761
761
|
def _update_history_from_sdk_result(
|
|
762
762
|
self, result, messages: list[dict[str, Any]]
|
|
763
763
|
) -> None:
|
|
764
|
-
"""Extract conversation history from SDK RunResult and update _history.
|
|
764
|
+
"""Extract conversation history from SDK RunResult and update _history.
|
|
765
|
+
|
|
766
|
+
The result is stored in **chat-completions shape** because it is
|
|
767
|
+
replayed into the next ``chat()`` call. A ``role: "tool"`` entry is
|
|
768
|
+
only legal when an assistant turn carrying the matching ``tool_calls``
|
|
769
|
+
precedes it, so the tool call is recorded with its real id, name and
|
|
770
|
+
arguments rather than a ``"Tool call: x"`` label — strict providers
|
|
771
|
+
(vLLM among them) reject the orphaned tool result outright.
|
|
772
|
+
"""
|
|
765
773
|
from agents.items import MessageOutputItem, ToolCallItem, ToolCallOutputItem
|
|
766
774
|
|
|
767
775
|
history = []
|
|
768
|
-
last_tool_name: str | None = None
|
|
769
776
|
for item in getattr(result, "new_items", None) or []:
|
|
770
777
|
if isinstance(item, MessageOutputItem):
|
|
771
778
|
# Extract text content from message
|
|
@@ -779,31 +786,40 @@ class Agent:
|
|
|
779
786
|
"content": " ".join(text_parts),
|
|
780
787
|
})
|
|
781
788
|
elif isinstance(item, ToolCallItem):
|
|
782
|
-
|
|
783
|
-
|
|
789
|
+
call_id = (
|
|
790
|
+
_item_field(item, "call_id") or f"call_{len(history)}"
|
|
791
|
+
)
|
|
792
|
+
name = item.tool_name or _item_field(item, "name") or ""
|
|
784
793
|
history.append({
|
|
785
794
|
"role": "assistant",
|
|
786
|
-
"content":
|
|
795
|
+
"content": None,
|
|
796
|
+
"tool_calls": [
|
|
797
|
+
{
|
|
798
|
+
"id": call_id,
|
|
799
|
+
"type": "function",
|
|
800
|
+
"function": {
|
|
801
|
+
"name": name,
|
|
802
|
+
"arguments": (
|
|
803
|
+
_item_field(item, "arguments") or "{}"
|
|
804
|
+
),
|
|
805
|
+
},
|
|
806
|
+
}
|
|
807
|
+
],
|
|
787
808
|
})
|
|
788
809
|
elif isinstance(item, ToolCallOutputItem):
|
|
789
|
-
# ``raw_item`` is a ``function_call_output`` dict
|
|
790
|
-
#
|
|
791
|
-
# wiped the history after every
|
|
792
|
-
#
|
|
793
|
-
#
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
else:
|
|
798
|
-
call_id = getattr(raw, "call_id", None) or getattr(
|
|
799
|
-
raw, "id", None
|
|
800
|
-
)
|
|
810
|
+
# ``raw_item`` is a ``function_call_output`` — a dict in some
|
|
811
|
+
# paths and a pydantic model in others. Reading it by attribute
|
|
812
|
+
# alone raised AttributeError and wiped the history after every
|
|
813
|
+
# tool call; reading it as a dict alone silently yielded no
|
|
814
|
+
# call_id, orphaning the result.
|
|
815
|
+
call_id = _item_field(item, "call_id") or _item_field(
|
|
816
|
+
item, "id"
|
|
817
|
+
)
|
|
801
818
|
output = item.output
|
|
802
819
|
history.append(
|
|
803
820
|
{
|
|
804
821
|
"role": "tool",
|
|
805
822
|
"tool_call_id": call_id,
|
|
806
|
-
"name": last_tool_name,
|
|
807
823
|
"content": output
|
|
808
824
|
if isinstance(output, str)
|
|
809
825
|
else str(output),
|
|
@@ -1150,6 +1166,26 @@ def _safe_final_output(result: Any) -> str:
|
|
|
1150
1166
|
return output if isinstance(output, str) else ""
|
|
1151
1167
|
|
|
1152
1168
|
|
|
1169
|
+
def _item_field(item: Any, name: str) -> Any:
|
|
1170
|
+
"""Read ``name`` off an SDK item whose ``raw_item`` may be dict or model.
|
|
1171
|
+
|
|
1172
|
+
``ToolCallItem.raw_item`` is a ``ResponseFunctionToolCall`` object when the
|
|
1173
|
+
Runner built it, and a plain ``function_call`` dict in other paths. Reading
|
|
1174
|
+
only one of the two silently produced empty tool arguments, so a replayed
|
|
1175
|
+
history told the model it had called a tool with ``{}``.
|
|
1176
|
+
"""
|
|
1177
|
+
raw = getattr(item, "raw_item", None)
|
|
1178
|
+
if isinstance(raw, dict):
|
|
1179
|
+
value = raw.get(name)
|
|
1180
|
+
if value is not None:
|
|
1181
|
+
return value
|
|
1182
|
+
elif raw is not None:
|
|
1183
|
+
value = getattr(raw, name, None)
|
|
1184
|
+
if value is not None:
|
|
1185
|
+
return value
|
|
1186
|
+
return getattr(item, name, None)
|
|
1187
|
+
|
|
1188
|
+
|
|
1153
1189
|
def _run_coroutine(coro: Any) -> Any:
|
|
1154
1190
|
"""Run ``coro`` to completion from sync code, notebooks included.
|
|
1155
1191
|
|
|
@@ -31,6 +31,8 @@ from pydantic_monty import (
|
|
|
31
31
|
ResourceLimits,
|
|
32
32
|
)
|
|
33
33
|
|
|
34
|
+
from regcode.async_utils import run_coroutine_blocking
|
|
35
|
+
|
|
34
36
|
|
|
35
37
|
@dataclass
|
|
36
38
|
class MontySandboxConfig:
|
|
@@ -200,21 +202,21 @@ class MontySandbox:
|
|
|
200
202
|
# Create Monty runner
|
|
201
203
|
monty = Monty(code)
|
|
202
204
|
|
|
203
|
-
# Execute code - always use asyncio.run() for full isolation
|
|
204
|
-
# This avoids "no running event loop" errors that occur when
|
|
205
|
-
# the Monty library checks for a running loop at the process level
|
|
206
205
|
# Create callback outside async scope so we can access .output
|
|
207
206
|
cb = CollectString()
|
|
208
207
|
|
|
208
|
+
async def _async_run():
|
|
209
|
+
return await monty.run_async(
|
|
210
|
+
external_functions=funcs,
|
|
211
|
+
limits=limits,
|
|
212
|
+
print_callback=cb,
|
|
213
|
+
)
|
|
214
|
+
|
|
209
215
|
try:
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
print_callback=cb,
|
|
215
|
-
)
|
|
216
|
-
|
|
217
|
-
result = asyncio.run(_async_run())
|
|
216
|
+
# Must not call asyncio.run() directly: tools execute inside the
|
|
217
|
+
# SDK Runner's event loop, where a nested asyncio.run() raises
|
|
218
|
+
# "cannot be called from a running event loop".
|
|
219
|
+
result = run_coroutine_blocking(_async_run)
|
|
218
220
|
except Exception as e:
|
|
219
221
|
return MontySandboxResult(
|
|
220
222
|
stdout=f"SANDBOX_ERROR: {str(e)}",
|
|
@@ -96,11 +96,14 @@ class WebCrawlerTool(BaseTool):
|
|
|
96
96
|
exit_code=1,
|
|
97
97
|
)
|
|
98
98
|
|
|
99
|
-
import
|
|
99
|
+
from regcode.async_utils import run_coroutine_blocking
|
|
100
100
|
|
|
101
101
|
try:
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
# Loop-safe: this tool runs inside the SDK Runner's event loop.
|
|
103
|
+
result = run_coroutine_blocking(
|
|
104
|
+
lambda: self._crawl_async(
|
|
105
|
+
url, markdown, word_count_threshold, only_text
|
|
106
|
+
)
|
|
104
107
|
)
|
|
105
108
|
return result
|
|
106
109
|
except Exception as e:
|
|
@@ -159,7 +159,15 @@ class TestAgentMultimodal:
|
|
|
159
159
|
messages = mock_completion.call_args[1]["messages"]
|
|
160
160
|
user_msg = [m for m in messages if m["role"] == "user"][0]
|
|
161
161
|
assert isinstance(user_msg["content"], list)
|
|
162
|
-
|
|
162
|
+
# The SDK's converter normalises image parts (it fills in
|
|
163
|
+
# detail="auto"), so assert the payload survives rather than
|
|
164
|
+
# exact dict equality.
|
|
165
|
+
parts = user_msg["content"]
|
|
166
|
+
assert [p["type"] for p in parts] == ["image_url", "text"]
|
|
167
|
+
assert (
|
|
168
|
+
parts[0]["image_url"]["url"] == "data:image/png;base64,abc"
|
|
169
|
+
)
|
|
170
|
+
assert parts[1]["text"] == "Describe this image."
|
|
163
171
|
|
|
164
172
|
def test_chat_with_invalid_message_type(self):
|
|
165
173
|
from regcode.main import Agent
|
|
@@ -73,7 +73,14 @@ def test_agent_chat_with_tool_use():
|
|
|
73
73
|
"assistant",
|
|
74
74
|
]
|
|
75
75
|
assert agent.history[0]["content"] == "calculate 1+1"
|
|
76
|
-
|
|
76
|
+
# The assistant turn must carry a real tool_calls entry: a role="tool"
|
|
77
|
+
# result is only legal when the matching call precedes it, and strict
|
|
78
|
+
# providers reject the orphan otherwise.
|
|
79
|
+
assert agent.history[1]["tool_calls"][0]["function"]["name"] == "python_code"
|
|
80
|
+
assert agent.history[1]["tool_calls"][0]["function"]["arguments"] == (
|
|
81
|
+
'{"code": "print(1)" }'
|
|
82
|
+
)
|
|
83
|
+
assert agent.history[2]["tool_call_id"] == "call_123"
|
|
77
84
|
assert agent.history[3]["content"] == "Done!"
|
|
78
85
|
|
|
79
86
|
|
|
@@ -461,3 +461,295 @@ def test_module_level_helpers_exist():
|
|
|
461
461
|
for gone in ("_stream", "_chat_loop", "_execute_tool_calls"):
|
|
462
462
|
assert not hasattr(Agent, gone), f"{gone} should have been removed"
|
|
463
463
|
assert not hasattr(main_mod, "_convert_registry_tools_to_sdk_tools")
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
# ── CLI regression: Responses items must never reach litellm ────────────
|
|
467
|
+
|
|
468
|
+
# Keys that belong to the Responses API. If one of these appears in a message
|
|
469
|
+
# handed to litellm, the provider answers
|
|
470
|
+
# "ChatCompletionContentPartTextParam.type Input should be 'text'
|
|
471
|
+
# [input_value='output_text']"
|
|
472
|
+
# which surfaced in the CLI as "Agent error! litellm.BadRequestError".
|
|
473
|
+
_RESPONSES_ONLY_KEYS = {"output_text", "function_call", "function_call_output"}
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def _assert_chat_completions_shape(messages: list[dict], turn: int) -> None:
|
|
477
|
+
"""Fail loudly if any Responses-API item leaked into a litellm request."""
|
|
478
|
+
for msg in messages:
|
|
479
|
+
assert "type" not in msg or msg["type"] in (
|
|
480
|
+
"message",
|
|
481
|
+
), f"turn {turn}: Responses item leaked: {msg}"
|
|
482
|
+
content = msg.get("content")
|
|
483
|
+
if isinstance(content, list):
|
|
484
|
+
for part in content:
|
|
485
|
+
assert part.get("type") not in _RESPONSES_ONLY_KEYS, (
|
|
486
|
+
f"turn {turn}: Responses content part leaked: {part}"
|
|
487
|
+
)
|
|
488
|
+
if msg.get("role") == "tool":
|
|
489
|
+
assert msg.get("tool_call_id"), (
|
|
490
|
+
f"turn {turn}: tool result without tool_call_id: {msg}"
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
class TestToolTurnMessageShape:
|
|
495
|
+
"""The Runner replays the assistant's own output back to the model.
|
|
496
|
+
|
|
497
|
+
Every test that existed before made the model return *either* text *or* a
|
|
498
|
+
tool call, so the second turn — the one carrying the replayed assistant
|
|
499
|
+
item — never happened, and the CLI broke on the first real conversation.
|
|
500
|
+
"""
|
|
501
|
+
|
|
502
|
+
def test_text_with_tool_call_replays_as_chat_messages(self):
|
|
503
|
+
agent = Agent(config=Config(), directory=".")
|
|
504
|
+
with patch("regcode.agents_backend.litellm.completion") as m:
|
|
505
|
+
m.side_effect = [
|
|
506
|
+
completion_response(
|
|
507
|
+
"Let me take a quick peek.",
|
|
508
|
+
tool_calls=[tool_call("c1", "system_info", "{}")],
|
|
509
|
+
),
|
|
510
|
+
completion_response("done"),
|
|
511
|
+
]
|
|
512
|
+
agent.chat("hi")
|
|
513
|
+
turns = [c.kwargs["messages"] for c in m.call_args_list]
|
|
514
|
+
|
|
515
|
+
assert len(turns) == 2, "the tool turn must be followed by a second call"
|
|
516
|
+
for i, messages in enumerate(turns):
|
|
517
|
+
_assert_chat_completions_shape(messages, i)
|
|
518
|
+
|
|
519
|
+
replayed = turns[1][2]
|
|
520
|
+
assert replayed["role"] == "assistant"
|
|
521
|
+
# The SDK's converter merges the text and the call into one turn.
|
|
522
|
+
assert replayed["content"] == "Let me take a quick peek."
|
|
523
|
+
assert replayed["tool_calls"][0]["id"] == "c1"
|
|
524
|
+
assert turns[1][3]["role"] == "tool"
|
|
525
|
+
assert turns[1][3]["tool_call_id"] == "c1"
|
|
526
|
+
|
|
527
|
+
def test_streaming_text_with_tool_call(self):
|
|
528
|
+
agent = Agent(config=Config(), directory=".")
|
|
529
|
+
chunks: list[str] = []
|
|
530
|
+
with patch(
|
|
531
|
+
"regcode.agents_backend.litellm.acompletion", new_callable=AsyncMock
|
|
532
|
+
) as m:
|
|
533
|
+
m.side_effect = [
|
|
534
|
+
stream_chunks(
|
|
535
|
+
"peeking", tool_calls=[tool_call("c1", "system_info", "{}")]
|
|
536
|
+
),
|
|
537
|
+
stream_chunks("streamed done"),
|
|
538
|
+
]
|
|
539
|
+
out = agent.chat("hi", stream=True, on_text_chunk=chunks.append)
|
|
540
|
+
|
|
541
|
+
assert out == "peekingstreamed done"
|
|
542
|
+
assert "".join(chunks) == out
|
|
543
|
+
for i, call in enumerate(m.call_args_list):
|
|
544
|
+
_assert_chat_completions_shape(call.kwargs["messages"], i)
|
|
545
|
+
|
|
546
|
+
def test_second_chat_call_replays_history_validly(self):
|
|
547
|
+
"""Multi-turn is where the hand-built history was silently invalid.
|
|
548
|
+
|
|
549
|
+
A ``role: "tool"`` entry is only legal when an assistant turn carrying
|
|
550
|
+
the matching ``tool_calls`` precedes it; strict providers (vLLM) reject
|
|
551
|
+
the orphan outright.
|
|
552
|
+
"""
|
|
553
|
+
agent = Agent(config=Config(), directory=".")
|
|
554
|
+
with patch("regcode.agents_backend.litellm.completion") as m:
|
|
555
|
+
m.side_effect = [
|
|
556
|
+
completion_response(
|
|
557
|
+
"peeking", tool_calls=[tool_call("c1", "system_info", "{}")]
|
|
558
|
+
),
|
|
559
|
+
completion_response("first done"),
|
|
560
|
+
completion_response(
|
|
561
|
+
"looking again",
|
|
562
|
+
tool_calls=[tool_call("c2", "list_dir", '{"path": "."}')],
|
|
563
|
+
),
|
|
564
|
+
completion_response("second done"),
|
|
565
|
+
]
|
|
566
|
+
assert agent.chat("hi") == "first done"
|
|
567
|
+
assert agent.chat("again") == "second done"
|
|
568
|
+
turns = [c.kwargs["messages"] for c in m.call_args_list]
|
|
569
|
+
|
|
570
|
+
for i, messages in enumerate(turns):
|
|
571
|
+
_assert_chat_completions_shape(messages, i)
|
|
572
|
+
|
|
573
|
+
last = turns[-1]
|
|
574
|
+
tool_entries = [msg for msg in last if msg.get("role") == "tool"]
|
|
575
|
+
assert tool_entries, "history should carry the earlier tool result"
|
|
576
|
+
assistant_with_calls = [
|
|
577
|
+
msg for msg in last if msg.get("role") == "assistant" and msg.get(
|
|
578
|
+
"tool_calls"
|
|
579
|
+
)
|
|
580
|
+
]
|
|
581
|
+
known_ids = {
|
|
582
|
+
tc["id"]
|
|
583
|
+
for msg in assistant_with_calls
|
|
584
|
+
for tc in msg["tool_calls"]
|
|
585
|
+
}
|
|
586
|
+
for msg in tool_entries:
|
|
587
|
+
assert msg["tool_call_id"] in known_ids, (
|
|
588
|
+
f"orphaned tool result: {msg['tool_call_id']} not in {known_ids}"
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
def test_history_preserves_real_tool_arguments(self):
|
|
592
|
+
"""A replayed history must not claim the tool was called with ``{}``."""
|
|
593
|
+
agent = Agent(config=Config(), directory=".")
|
|
594
|
+
with patch("regcode.agents_backend.litellm.completion") as m:
|
|
595
|
+
m.side_effect = [
|
|
596
|
+
completion_response(
|
|
597
|
+
tool_calls=[
|
|
598
|
+
tool_call("c1", "python_code", '{"code": "print(1)"}')
|
|
599
|
+
]
|
|
600
|
+
),
|
|
601
|
+
completion_response("done"),
|
|
602
|
+
]
|
|
603
|
+
agent.chat("calc")
|
|
604
|
+
|
|
605
|
+
call = agent.history[1]["tool_calls"][0]
|
|
606
|
+
assert call["function"]["arguments"] == '{"code": "print(1)"}'
|
|
607
|
+
assert call["function"]["name"] == "python_code"
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
class TestLegacyMessageShapes:
|
|
611
|
+
"""``chat(messages=...)`` is public API and accepts plain chat dicts."""
|
|
612
|
+
|
|
613
|
+
def test_role_tool_history_is_accepted(self):
|
|
614
|
+
from regcode.agents_backend import _expand_input_items
|
|
615
|
+
|
|
616
|
+
expanded = _expand_input_items(
|
|
617
|
+
[{"role": "tool", "tool_call_id": "c9", "content": "output text"}]
|
|
618
|
+
)
|
|
619
|
+
assert expanded == [
|
|
620
|
+
{
|
|
621
|
+
"type": "function_call_output",
|
|
622
|
+
"call_id": "c9",
|
|
623
|
+
"output": "output text",
|
|
624
|
+
}
|
|
625
|
+
]
|
|
626
|
+
|
|
627
|
+
def test_assistant_tool_calls_are_split(self):
|
|
628
|
+
from regcode.agents_backend import _expand_input_items
|
|
629
|
+
|
|
630
|
+
expanded = _expand_input_items(
|
|
631
|
+
[
|
|
632
|
+
{
|
|
633
|
+
"role": "assistant",
|
|
634
|
+
"content": "thinking",
|
|
635
|
+
"tool_calls": [
|
|
636
|
+
{
|
|
637
|
+
"id": "c1",
|
|
638
|
+
"type": "function",
|
|
639
|
+
"function": {
|
|
640
|
+
"name": "read_file",
|
|
641
|
+
"arguments": '{"path": "a"}',
|
|
642
|
+
},
|
|
643
|
+
}
|
|
644
|
+
],
|
|
645
|
+
}
|
|
646
|
+
]
|
|
647
|
+
)
|
|
648
|
+
assert expanded[0]["type"] == "function_call"
|
|
649
|
+
assert expanded[0]["name"] == "read_file"
|
|
650
|
+
assert expanded[1]["content"][0]["text"] == "thinking"
|
|
651
|
+
|
|
652
|
+
def test_plain_dicts_pass_through(self):
|
|
653
|
+
from regcode.agents_backend import _expand_input_items
|
|
654
|
+
|
|
655
|
+
items = [{"role": "user", "content": "hi"}]
|
|
656
|
+
assert _expand_input_items(items) == items
|
|
657
|
+
|
|
658
|
+
def test_chat_with_prebuilt_messages_survives_tool_history(self):
|
|
659
|
+
"""code_review()/GitKnecht pass messages built from saved history."""
|
|
660
|
+
agent = Agent(config=Config(), directory=".")
|
|
661
|
+
messages = [
|
|
662
|
+
{"role": "system", "content": "SYS"},
|
|
663
|
+
{"role": "user", "content": "start"},
|
|
664
|
+
{
|
|
665
|
+
"role": "assistant",
|
|
666
|
+
"content": None,
|
|
667
|
+
"tool_calls": [
|
|
668
|
+
{
|
|
669
|
+
"id": "c1",
|
|
670
|
+
"type": "function",
|
|
671
|
+
"function": {
|
|
672
|
+
"name": "system_info",
|
|
673
|
+
"arguments": "{}",
|
|
674
|
+
},
|
|
675
|
+
}
|
|
676
|
+
],
|
|
677
|
+
},
|
|
678
|
+
{"role": "tool", "tool_call_id": "c1", "content": "OS: Linux"},
|
|
679
|
+
]
|
|
680
|
+
with patch("regcode.agents_backend.litellm.completion") as m:
|
|
681
|
+
m.return_value = completion_response("ok")
|
|
682
|
+
agent.chat(messages=messages)
|
|
683
|
+
sent = m.call_args.kwargs["messages"]
|
|
684
|
+
|
|
685
|
+
_assert_chat_completions_shape(sent, 0)
|
|
686
|
+
assert any(msg.get("role") == "tool" for msg in sent)
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
class TestToolsRunInsideEventLoop:
|
|
690
|
+
"""Tools execute inside the Runner's loop, so asyncio.run() is illegal.
|
|
691
|
+
|
|
692
|
+
``python_code``/``run_script`` (via MontySandbox) and ``web_crawl`` all
|
|
693
|
+
called ``asyncio.run()`` directly and every one of them returned
|
|
694
|
+
``SANDBOX_ERROR: asyncio.run() cannot be called from a running event loop``.
|
|
695
|
+
"""
|
|
696
|
+
|
|
697
|
+
def test_python_code_executes_under_runner(self):
|
|
698
|
+
agent = Agent(config=Config(), directory=".")
|
|
699
|
+
with patch("regcode.agents_backend.litellm.completion") as m:
|
|
700
|
+
m.side_effect = [
|
|
701
|
+
completion_response(
|
|
702
|
+
tool_calls=[
|
|
703
|
+
tool_call("c1", "python_code", '{"code": "print(42)"}')
|
|
704
|
+
]
|
|
705
|
+
),
|
|
706
|
+
completion_response("done"),
|
|
707
|
+
]
|
|
708
|
+
agent.chat("run it")
|
|
709
|
+
|
|
710
|
+
tool_msg = next(h for h in agent.history if h["role"] == "tool")
|
|
711
|
+
assert "SANDBOX_ERROR" not in tool_msg["content"]
|
|
712
|
+
assert "42" in tool_msg["content"]
|
|
713
|
+
assert "exit_code: 0" in tool_msg["content"]
|
|
714
|
+
|
|
715
|
+
def test_run_coroutine_blocking_outside_loop(self):
|
|
716
|
+
import asyncio
|
|
717
|
+
|
|
718
|
+
from regcode.async_utils import run_coroutine_blocking
|
|
719
|
+
|
|
720
|
+
async def work() -> int:
|
|
721
|
+
await asyncio.sleep(0)
|
|
722
|
+
return 7
|
|
723
|
+
|
|
724
|
+
assert run_coroutine_blocking(work) == 7
|
|
725
|
+
|
|
726
|
+
def test_run_coroutine_blocking_inside_loop(self):
|
|
727
|
+
import asyncio
|
|
728
|
+
|
|
729
|
+
from regcode.async_utils import run_coroutine_blocking
|
|
730
|
+
|
|
731
|
+
async def work() -> int:
|
|
732
|
+
await asyncio.sleep(0)
|
|
733
|
+
return 7
|
|
734
|
+
|
|
735
|
+
async def outer() -> int:
|
|
736
|
+
# A running loop must not make the nested call fail.
|
|
737
|
+
return run_coroutine_blocking(work)
|
|
738
|
+
|
|
739
|
+
assert asyncio.run(outer()) == 7
|
|
740
|
+
|
|
741
|
+
def test_run_coroutine_blocking_propagates_errors(self):
|
|
742
|
+
import asyncio
|
|
743
|
+
|
|
744
|
+
import pytest
|
|
745
|
+
|
|
746
|
+
from regcode.async_utils import run_coroutine_blocking
|
|
747
|
+
|
|
748
|
+
async def boom() -> None:
|
|
749
|
+
raise ValueError("kaboom")
|
|
750
|
+
|
|
751
|
+
async def outer() -> None:
|
|
752
|
+
run_coroutine_blocking(boom)
|
|
753
|
+
|
|
754
|
+
with pytest.raises(ValueError, match="kaboom"):
|
|
755
|
+
asyncio.run(outer())
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
version = 1
|
|
2
|
-
revision =
|
|
2
|
+
revision = 5
|
|
3
3
|
requires-python = ">=3.10"
|
|
4
4
|
resolution-markers = [
|
|
5
5
|
"python_full_version >= '3.14' and sys_platform == 'win32'",
|
|
@@ -682,7 +682,7 @@ resolution-markers = [
|
|
|
682
682
|
"python_full_version < '3.11'",
|
|
683
683
|
]
|
|
684
684
|
dependencies = [
|
|
685
|
-
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }
|
|
685
|
+
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } },
|
|
686
686
|
]
|
|
687
687
|
sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" }
|
|
688
688
|
wheels = [
|
|
@@ -756,7 +756,7 @@ resolution-markers = [
|
|
|
756
756
|
"python_full_version == '3.11.*'",
|
|
757
757
|
]
|
|
758
758
|
dependencies = [
|
|
759
|
-
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version
|
|
759
|
+
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
|
|
760
760
|
{ name = "numpy", version = "2.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
|
|
761
761
|
]
|
|
762
762
|
sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" }
|
|
@@ -1001,7 +1001,7 @@ name = "exceptiongroup"
|
|
|
1001
1001
|
version = "1.3.1"
|
|
1002
1002
|
source = { registry = "https://pypi.org/simple" }
|
|
1003
1003
|
dependencies = [
|
|
1004
|
-
{ name = "typing-extensions"
|
|
1004
|
+
{ name = "typing-extensions" },
|
|
1005
1005
|
]
|
|
1006
1006
|
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
|
|
1007
1007
|
wheels = [
|
|
@@ -1496,7 +1496,7 @@ name = "gunicorn"
|
|
|
1496
1496
|
version = "26.0.0"
|
|
1497
1497
|
source = { registry = "https://pypi.org/simple" }
|
|
1498
1498
|
dependencies = [
|
|
1499
|
-
{ name = "packaging"
|
|
1499
|
+
{ name = "packaging" },
|
|
1500
1500
|
]
|
|
1501
1501
|
sdist = { url = "https://files.pythonhosted.org/packages/6d/b7/a4a3f632f823e432ce6bc65f62961b7980c898c77f075a2f7118cb3846fe/gunicorn-26.0.0.tar.gz", hash = "sha256:ca9346f85e3a4aeeb64d491045c16b9a35647abd37ea15efe53080eb8b090baf", size = 727286, upload-time = "2026-05-05T06:38:25.529Z" }
|
|
1502
1502
|
wheels = [
|
|
@@ -2228,15 +2228,15 @@ resolution-markers = [
|
|
|
2228
2228
|
"python_full_version < '3.11'",
|
|
2229
2229
|
]
|
|
2230
2230
|
dependencies = [
|
|
2231
|
-
{ name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }
|
|
2232
|
-
{ name = "cycler"
|
|
2233
|
-
{ name = "fonttools"
|
|
2234
|
-
{ name = "kiwisolver"
|
|
2235
|
-
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }
|
|
2236
|
-
{ name = "packaging"
|
|
2237
|
-
{ name = "pillow"
|
|
2238
|
-
{ name = "pyparsing"
|
|
2239
|
-
{ name = "python-dateutil"
|
|
2231
|
+
{ name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" } },
|
|
2232
|
+
{ name = "cycler" },
|
|
2233
|
+
{ name = "fonttools" },
|
|
2234
|
+
{ name = "kiwisolver" },
|
|
2235
|
+
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } },
|
|
2236
|
+
{ name = "packaging" },
|
|
2237
|
+
{ name = "pillow" },
|
|
2238
|
+
{ name = "pyparsing" },
|
|
2239
|
+
{ name = "python-dateutil" },
|
|
2240
2240
|
]
|
|
2241
2241
|
sdist = { url = "https://files.pythonhosted.org/packages/63/1b/4be5be87d43d327a0cf4de1a56e86f7f84c89312452406cf122efe2839e6/matplotlib-3.10.9.tar.gz", hash = "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358", size = 34811233, upload-time = "2026-04-24T00:14:13.539Z" }
|
|
2242
2242
|
wheels = [
|
|
@@ -2308,16 +2308,16 @@ resolution-markers = [
|
|
|
2308
2308
|
"python_full_version == '3.11.*'",
|
|
2309
2309
|
]
|
|
2310
2310
|
dependencies = [
|
|
2311
|
-
{ name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }
|
|
2312
|
-
{ name = "cycler"
|
|
2313
|
-
{ name = "fonttools"
|
|
2314
|
-
{ name = "kiwisolver"
|
|
2315
|
-
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version
|
|
2311
|
+
{ name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" } },
|
|
2312
|
+
{ name = "cycler" },
|
|
2313
|
+
{ name = "fonttools" },
|
|
2314
|
+
{ name = "kiwisolver" },
|
|
2315
|
+
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
|
|
2316
2316
|
{ name = "numpy", version = "2.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
|
|
2317
|
-
{ name = "packaging"
|
|
2318
|
-
{ name = "pillow"
|
|
2319
|
-
{ name = "pyparsing"
|
|
2320
|
-
{ name = "python-dateutil"
|
|
2317
|
+
{ name = "packaging" },
|
|
2318
|
+
{ name = "pillow" },
|
|
2319
|
+
{ name = "pyparsing" },
|
|
2320
|
+
{ name = "python-dateutil" },
|
|
2321
2321
|
]
|
|
2322
2322
|
sdist = { url = "https://files.pythonhosted.org/packages/1f/24/080c99d223d158d3a8902769269ab6da5b50f7a0e6e072513907e02b7a6c/matplotlib-3.11.0.tar.gz", hash = "sha256:68c0c7be01b30dcca3638934f7f591df73401235cbdbf0d1ab1c71e7db7f8b57", size = 33251176, upload-time = "2026-06-12T02:29:15.508Z" }
|
|
2323
2323
|
wheels = [
|
|
@@ -3962,7 +3962,7 @@ wheels = [
|
|
|
3962
3962
|
|
|
3963
3963
|
[[package]]
|
|
3964
3964
|
name = "regcode"
|
|
3965
|
-
version = "0.
|
|
3965
|
+
version = "0.12.1"
|
|
3966
3966
|
source = { editable = "." }
|
|
3967
3967
|
dependencies = [
|
|
3968
3968
|
{ name = "boto3" },
|
|
@@ -4481,10 +4481,10 @@ resolution-markers = [
|
|
|
4481
4481
|
"python_full_version < '3.11'",
|
|
4482
4482
|
]
|
|
4483
4483
|
dependencies = [
|
|
4484
|
-
{ name = "joblib"
|
|
4485
|
-
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }
|
|
4486
|
-
{ name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }
|
|
4487
|
-
{ name = "threadpoolctl"
|
|
4484
|
+
{ name = "joblib" },
|
|
4485
|
+
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } },
|
|
4486
|
+
{ name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" } },
|
|
4487
|
+
{ name = "threadpoolctl" },
|
|
4488
4488
|
]
|
|
4489
4489
|
sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" }
|
|
4490
4490
|
wheels = [
|
|
@@ -4532,13 +4532,13 @@ resolution-markers = [
|
|
|
4532
4532
|
"python_full_version == '3.11.*'",
|
|
4533
4533
|
]
|
|
4534
4534
|
dependencies = [
|
|
4535
|
-
{ name = "joblib"
|
|
4536
|
-
{ name = "narwhals"
|
|
4537
|
-
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version
|
|
4535
|
+
{ name = "joblib" },
|
|
4536
|
+
{ name = "narwhals" },
|
|
4537
|
+
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
|
|
4538
4538
|
{ name = "numpy", version = "2.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
|
|
4539
|
-
{ name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version
|
|
4539
|
+
{ name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
|
|
4540
4540
|
{ name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
|
|
4541
|
-
{ name = "threadpoolctl"
|
|
4541
|
+
{ name = "threadpoolctl" },
|
|
4542
4542
|
]
|
|
4543
4543
|
sdist = { url = "https://files.pythonhosted.org/packages/fa/6f/37092bdb25f712817231799fc5674d8e704066a8a70c1d2d40517e18b4ab/scikit_learn-1.9.0.tar.gz", hash = "sha256:8833266989d3a5110178a9fae30783675460724d0e1efb13b14901d2c660c557", size = 7750767, upload-time = "2026-06-02T11:54:32.706Z" }
|
|
4544
4544
|
wheels = [
|
|
@@ -4582,7 +4582,7 @@ resolution-markers = [
|
|
|
4582
4582
|
"python_full_version < '3.11'",
|
|
4583
4583
|
]
|
|
4584
4584
|
dependencies = [
|
|
4585
|
-
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }
|
|
4585
|
+
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } },
|
|
4586
4586
|
]
|
|
4587
4587
|
sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" }
|
|
4588
4588
|
wheels = [
|
|
@@ -4641,7 +4641,7 @@ resolution-markers = [
|
|
|
4641
4641
|
"python_full_version == '3.11.*'",
|
|
4642
4642
|
]
|
|
4643
4643
|
dependencies = [
|
|
4644
|
-
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }
|
|
4644
|
+
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } },
|
|
4645
4645
|
]
|
|
4646
4646
|
sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" }
|
|
4647
4647
|
wheels = [
|
|
@@ -4718,7 +4718,7 @@ resolution-markers = [
|
|
|
4718
4718
|
"python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'win32'",
|
|
4719
4719
|
]
|
|
4720
4720
|
dependencies = [
|
|
4721
|
-
{ name = "numpy", version = "2.5.0", source = { registry = "https://pypi.org/simple" }
|
|
4721
|
+
{ name = "numpy", version = "2.5.0", source = { registry = "https://pypi.org/simple" } },
|
|
4722
4722
|
]
|
|
4723
4723
|
sdist = { url = "https://files.pythonhosted.org/packages/a7/25/c2700dfaf6442b4effaa91af24ebce5dc9d31bb4a69706313aae70d72cd0/scipy-1.18.0.tar.gz", hash = "sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378", size = 30774447, upload-time = "2026-06-19T15:01:43.456Z" }
|
|
4724
4724
|
wheels = [
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|