From a4ba740149bc9f0022173a4c10396ca4d60f5c42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Cartagena?= Date: Sun, 19 Jul 2026 16:53:38 +0200 Subject: [PATCH 1/7] fix(windows): make claude-cli provider work on Windows Two issues prevented the claude-cli LLM provider from running on Windows: - cwd="/tmp" is a POSIX-only path; on Windows this raises WinError 267 (invalid directory name). Replaced with tempfile.gettempdir() for both the claude and codex subprocess calls, which resolves correctly on all platforms. - The prompt was passed as a trailing argv element (["claude", "-p", "--output-format", "json", prompt]). Large prompts (tens of KB, common once a few input files are attached) exceed Windows' command-line length limit and raise WinError 206 ("filename or extension is too long"). Passing the prompt via stdin instead removes this ceiling. Bumped the subprocess timeout from 300s to 600s to give slower/CLI-based providers headroom, and set encoding="utf-8", errors="replace" explicitly since Windows' default locale encoding (cp1252) was raising UnicodeDecodeError on non-ASCII input/output. Tested on Windows 11 running full simulations (100+ agents, 15 rounds) end to end with LLM_PROVIDER=claude-cli. --- app/utils/llm_client.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/utils/llm_client.py b/app/utils/llm_client.py index f4fbb4c..a417d40 100644 --- a/app/utils/llm_client.py +++ b/app/utils/llm_client.py @@ -6,6 +6,7 @@ import re import subprocess import time +import tempfile from typing import Optional, Dict, Any, List from ..config import Config @@ -92,9 +93,11 @@ def _chat_claude_cli( try: result = subprocess.run( - ["claude", "-p", "--output-format", "json", prompt], - capture_output=True, text=True, timeout=300, - cwd="/tmp" + ["claude", "-p", "--output-format", "json"], + input=prompt, + capture_output=True, text=True, timeout=600, + encoding="utf-8", errors="replace", + cwd=tempfile.gettempdir() ) if result.returncode != 0: @@ -140,7 +143,7 @@ def _chat_codex_cli( ["codex", "exec", "--skip-git-repo-check"], input=prompt, capture_output=True, text=True, timeout=180, - cwd="/tmp" + cwd=tempfile.gettempdir() ) if result.returncode != 0: From 4d655bad2e76fea95d3e0b20a72e443d2dd90877 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Cartagena?= Date: Sun, 19 Jul 2026 16:53:45 +0200 Subject: [PATCH 2/7] fix(deps): pin rapidfuzz to 3.13.0 for Windows wheel availability rapidfuzz's latest release at time of writing (3.14.4) has no win_amd64 wheel published, which makes `uv sync` fail outright on Windows (no sdist build toolchain assumed). Overriding to 3.13.0, the latest version with a Windows wheel, unblocks installation. This can be relaxed/removed once upstream publishes Windows wheels for a newer version again. --- pyproject.toml | 3 ++ uv.lock | 87 +++++++++++++++++++++++++++++++------------------- 2 files changed, 57 insertions(+), 33 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ab4f119..f015395 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,3 +43,6 @@ dev = [ [tool.hatch.build.targets.wheel] packages = ["app"] + +[tool.uv] +override-dependencies = ["rapidfuzz==3.13.0"] diff --git a/uv.lock b/uv.lock index 8fa92c6..a936512 100644 --- a/uv.lock +++ b/uv.lock @@ -6,6 +6,9 @@ resolution-markers = [ "python_full_version < '3.12'", ] +[manifest] +overrides = [{ name = "rapidfuzz", specifier = "==3.13.0" }] + [[package]] name = "aiofiles" version = "25.1.0" @@ -352,37 +355,37 @@ wheels = [ [package.optional-dependencies] cublas = [ - { name = "nvidia-cublas", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cublas" }, ] cudart = [ - { name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cuda-runtime" }, ] cufft = [ - { name = "nvidia-cufft", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cufft" }, ] cufile = [ - { name = "nvidia-cufile", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cufile" }, ] cupti = [ - { name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cuda-cupti" }, ] curand = [ - { name = "nvidia-curand", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-curand" }, ] cusolver = [ - { name = "nvidia-cusolver", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cusolver" }, ] cusparse = [ - { name = "nvidia-cusparse", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cusparse" }, ] nvjitlink = [ - { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-nvjitlink" }, ] nvrtc = [ - { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cuda-nvrtc" }, ] nvtx = [ - { name = "nvidia-nvtx", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-nvtx" }, ] [[package]] @@ -1656,28 +1659,46 @@ wheels = [ [[package]] name = "rapidfuzz" -version = "3.14.4" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/11/b33c1d3f35c946a90184817727be4750d5ffa7c6d99bdd9400a8bb6ce9f4/rapidfuzz-3.14.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ce41a3a8ff4c672007e65c68f0e70662fac69469e57dfb5241e886ba3b536430", size = 1952370, upload-time = "2026-04-07T00:00:24.593Z" }, - { url = "https://files.pythonhosted.org/packages/5c/3b/e80612ea074fce99dad4b22e53a4577bdb115cf04cabe28ed2dc67740b8e/rapidfuzz-3.14.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ea968514d4377f85fd65719d14d1a694cf4336f69bde4bd917b4a02b7433e6d", size = 1159777, upload-time = "2026-04-07T00:00:26.591Z" }, - { url = "https://files.pythonhosted.org/packages/2a/a5/47b0f262f397e390fb4628a374a8760dac008b57848011020ce2f9eefff8/rapidfuzz-3.14.4-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6888bfae9d6efe3846cbb37a56c5883b3f738bb2de81060a9e7f17a145f91b26", size = 1383674, upload-time = "2026-04-07T00:00:28.561Z" }, - { url = "https://files.pythonhosted.org/packages/36/c2/24da03cb3c1fc199def4f4f42e513b2424d749a76574eb2d122f1e3e0310/rapidfuzz-3.14.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9766370de1cef0fe2867abbf3669500f19c664b9d682385d3e905b8fc3b9feb0", size = 3168903, upload-time = "2026-04-07T00:00:30.63Z" }, - { url = "https://files.pythonhosted.org/packages/8f/76/458e34b6992e4d3a64c846948b49577b35e0384e1a2138d8c15957e16b5c/rapidfuzz-3.14.4-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:4ccd1f37abefdd1e3d45ea6d65ff57192f76b2fd7ca1c6976314bbb65c85c7f3", size = 1478174, upload-time = "2026-04-07T00:00:32.337Z" }, - { url = "https://files.pythonhosted.org/packages/56/06/c274acd94662fddb4d1d376c0879423d0b8e48ebb6b1e40003ab09c013a6/rapidfuzz-3.14.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0c1cf90fbea5d6e992d2d3d4ff66212e7eb703ba7e4f1300f0a06717b91a21b3", size = 2402439, upload-time = "2026-04-07T00:00:34.312Z" }, - { url = "https://files.pythonhosted.org/packages/14/c4/86beae78be8640168e1239afcdbd789792f45eec4d3f6f7469f183bdda32/rapidfuzz-3.14.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:fe1011ccba99a83adfe47c089d1c4ce002a3368eb4cb03959e95a57ece6f59e0", size = 2511625, upload-time = "2026-04-07T00:00:36.631Z" }, - { url = "https://files.pythonhosted.org/packages/aa/3a/562bc5ca5ad7bcc8443d2b998999bf2c6e3cfd6df13974aea4ab1622e8e4/rapidfuzz-3.14.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f5b3c1c7ac2a4bd5b38c1b5e4c6fb594df55085d3d478c00e68ab7e397537517", size = 4275483, upload-time = "2026-04-07T00:00:39.086Z" }, - { url = "https://files.pythonhosted.org/packages/d5/81/186a4ac40e23de1256dd89100aab3d74da0463e7d11ac1a7461f24baebc9/rapidfuzz-3.14.4-cp311-cp311-win32.whl", hash = "sha256:e8e50e7ffe682e4b343ec0927e8fd305d03ba9edf68f6f458f753855e0eb8f5d", size = 1725589, upload-time = "2026-04-07T00:00:41.243Z" }, - { url = "https://files.pythonhosted.org/packages/17/72/71f7ebf61edbaecd4e595936107efe5c4c5bb7485efab0da51f00e224179/rapidfuzz-3.14.4-cp311-cp311-win_amd64.whl", hash = "sha256:2f525b81cd6d5132ec713f4369b8f05ba9bf42a54d1d0c349461f18f59242b42", size = 1545958, upload-time = "2026-04-07T00:00:43.154Z" }, - { url = "https://files.pythonhosted.org/packages/e0/36/92e31439ba7b2e9acd1979121fa8622aa9eb029da01a52ed1beb152e6423/rapidfuzz-3.14.4-cp311-cp311-win_arm64.whl", hash = "sha256:89ea914e81d0c1863c99fe9dfbee6010eecfc61cefa4f88e2af6ce976b171f9c", size = 816833, upload-time = "2026-04-07T00:00:44.901Z" }, - { url = "https://files.pythonhosted.org/packages/de/0d/33d2a36f210a503de648bf3c6b3692aac189e1fe42675f0c68bdf3815235/rapidfuzz-3.14.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:efdc67c5b2eb58e4e74ec1069b67c3df681e4ab07c1436f0a78f915a5ce97444", size = 1944599, upload-time = "2026-04-07T00:00:47.2Z" }, - { url = "https://files.pythonhosted.org/packages/5e/69/c3026437e339b90f71d4dc648cf82974419b6397aaf08c06bdec8ef4c547/rapidfuzz-3.14.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d66ac238f8b30c9e71456533c993b88487fcba9366b1e95f5f836c93efb0ba38", size = 1164293, upload-time = "2026-04-07T00:00:48.977Z" }, - { url = "https://files.pythonhosted.org/packages/18/75/587b46ab8a31b0081c9fe3ea2a3532b8527d551566f962df685c05ce630a/rapidfuzz-3.14.4-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe0b2895971834d5f59d2efd193810534834a131a5482333f9112c0cec2bd7af", size = 1371999, upload-time = "2026-04-07T00:00:50.701Z" }, - { url = "https://files.pythonhosted.org/packages/b2/99/b7fdf163d3a85a8860af736e959c0a3f8146f1795eacdfbc40083122d075/rapidfuzz-3.14.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:893c172f170ab401f52082505d240f166c338b3b16f962acd12b31e2462a3008", size = 3145710, upload-time = "2026-04-07T00:00:53.152Z" }, - { url = "https://files.pythonhosted.org/packages/f4/39/780651ebe59923a16479756f6d732cb950fd9da9257949fb1fa8159bb210/rapidfuzz-3.14.4-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:5fb9d58e8cfce5c5f9021da76924f2d5e86a21cb25bc4d694aa336b617a063e5", size = 1456303, upload-time = "2026-04-07T00:00:55.066Z" }, - { url = "https://files.pythonhosted.org/packages/59/be/a3427347176ad0b38ebf6c049c2885269d75f39e722d4559c8b0805306e3/rapidfuzz-3.14.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ca4449bc19e73a32279820ac0412dd1da66c2d6a5ae83f8abbf40ef77d793f03", size = 2389085, upload-time = "2026-04-07T00:00:57.019Z" }, - { url = "https://files.pythonhosted.org/packages/68/fb/3043f671dfc37eeceeb0d2398cffd86a595fb5e9c11739c6d8dc1c8c5527/rapidfuzz-3.14.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e1487b9ac54cdf835625d6347786b7b5df4e8f99102c8f5b2d221ab8229d1b50", size = 2493403, upload-time = "2026-04-07T00:00:58.935Z" }, - { url = "https://files.pythonhosted.org/packages/46/2a/8ef715f863f482a15c6b60e86ef7bfac84a3b5203ec03a957f24f909028a/rapidfuzz-3.14.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:38798a4cead67dd04917c5d63e3108f4bb8a1ca0ba022deef383d808908dec9e", size = 4251706, upload-time = "2026-04-07T00:01:01.077Z" }, +version = "3.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/f6/6895abc3a3d056b9698da3199b04c0e56226d530ae44a470edabf8b664f0/rapidfuzz-3.13.0.tar.gz", hash = "sha256:d2eaf3839e52cbcc0accbe9817a67b4b0fcf70aaeb229cfddc1c28061f9ce5d8", size = 57904226, upload-time = "2025-04-03T20:38:51.226Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/17/9be9eff5a3c7dfc831c2511262082c6786dca2ce21aa8194eef1cb71d67a/rapidfuzz-3.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d395a5cad0c09c7f096433e5fd4224d83b53298d53499945a9b0e5a971a84f3a", size = 1999453, upload-time = "2025-04-03T20:35:40.804Z" }, + { url = "https://files.pythonhosted.org/packages/75/67/62e57896ecbabe363f027d24cc769d55dd49019e576533ec10e492fcd8a2/rapidfuzz-3.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7b3eda607a019169f7187328a8d1648fb9a90265087f6903d7ee3a8eee01805", size = 1450881, upload-time = "2025-04-03T20:35:42.734Z" }, + { url = "https://files.pythonhosted.org/packages/96/5c/691c5304857f3476a7b3df99e91efc32428cbe7d25d234e967cc08346c13/rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98e0bfa602e1942d542de077baf15d658bd9d5dcfe9b762aff791724c1c38b70", size = 1422990, upload-time = "2025-04-03T20:35:45.158Z" }, + { url = "https://files.pythonhosted.org/packages/46/81/7a7e78f977496ee2d613154b86b203d373376bcaae5de7bde92f3ad5a192/rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bef86df6d59667d9655905b02770a0c776d2853971c0773767d5ef8077acd624", size = 5342309, upload-time = "2025-04-03T20:35:46.952Z" }, + { url = "https://files.pythonhosted.org/packages/51/44/12fdd12a76b190fe94bf38d252bb28ddf0ab7a366b943e792803502901a2/rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fedd316c165beed6307bf754dee54d3faca2c47e1f3bcbd67595001dfa11e969", size = 1656881, upload-time = "2025-04-03T20:35:49.954Z" }, + { url = "https://files.pythonhosted.org/packages/27/ae/0d933e660c06fcfb087a0d2492f98322f9348a28b2cc3791a5dbadf6e6fb/rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5158da7f2ec02a930be13bac53bb5903527c073c90ee37804090614cab83c29e", size = 1608494, upload-time = "2025-04-03T20:35:51.646Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2c/4b2f8aafdf9400e5599b6ed2f14bc26ca75f5a923571926ccbc998d4246a/rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b6f913ee4618ddb6d6f3e387b76e8ec2fc5efee313a128809fbd44e65c2bbb2", size = 3072160, upload-time = "2025-04-03T20:35:53.472Z" }, + { url = "https://files.pythonhosted.org/packages/60/7d/030d68d9a653c301114101c3003b31ce01cf2c3224034cd26105224cd249/rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d25fdbce6459ccbbbf23b4b044f56fbd1158b97ac50994eaae2a1c0baae78301", size = 2491549, upload-time = "2025-04-03T20:35:55.391Z" }, + { url = "https://files.pythonhosted.org/packages/8e/cd/7040ba538fc6a8ddc8816a05ecf46af9988b46c148ddd7f74fb0fb73d012/rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:25343ccc589a4579fbde832e6a1e27258bfdd7f2eb0f28cb836d6694ab8591fc", size = 7584142, upload-time = "2025-04-03T20:35:57.71Z" }, + { url = "https://files.pythonhosted.org/packages/c1/96/85f7536fbceb0aa92c04a1c37a3fc4fcd4e80649e9ed0fb585382df82edc/rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a9ad1f37894e3ffb76bbab76256e8a8b789657183870be11aa64e306bb5228fd", size = 2896234, upload-time = "2025-04-03T20:35:59.969Z" }, + { url = "https://files.pythonhosted.org/packages/55/fd/460e78438e7019f2462fe9d4ecc880577ba340df7974c8a4cfe8d8d029df/rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5dc71ef23845bb6b62d194c39a97bb30ff171389c9812d83030c1199f319098c", size = 3437420, upload-time = "2025-04-03T20:36:01.91Z" }, + { url = "https://files.pythonhosted.org/packages/cc/df/c3c308a106a0993befd140a414c5ea78789d201cf1dfffb8fd9749718d4f/rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b7f4c65facdb94f44be759bbd9b6dda1fa54d0d6169cdf1a209a5ab97d311a75", size = 4410860, upload-time = "2025-04-03T20:36:04.352Z" }, + { url = "https://files.pythonhosted.org/packages/75/ee/9d4ece247f9b26936cdeaae600e494af587ce9bf8ddc47d88435f05cfd05/rapidfuzz-3.13.0-cp311-cp311-win32.whl", hash = "sha256:b5104b62711565e0ff6deab2a8f5dbf1fbe333c5155abe26d2cfd6f1849b6c87", size = 1843161, upload-time = "2025-04-03T20:36:06.802Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5a/d00e1f63564050a20279015acb29ecaf41646adfacc6ce2e1e450f7f2633/rapidfuzz-3.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:9093cdeb926deb32a4887ebe6910f57fbcdbc9fbfa52252c10b56ef2efb0289f", size = 1629962, upload-time = "2025-04-03T20:36:09.133Z" }, + { url = "https://files.pythonhosted.org/packages/3b/74/0a3de18bc2576b794f41ccd07720b623e840fda219ab57091897f2320fdd/rapidfuzz-3.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:f70f646751b6aa9d05be1fb40372f006cc89d6aad54e9d79ae97bd1f5fce5203", size = 866631, upload-time = "2025-04-03T20:36:11.022Z" }, + { url = "https://files.pythonhosted.org/packages/13/4b/a326f57a4efed8f5505b25102797a58e37ee11d94afd9d9422cb7c76117e/rapidfuzz-3.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a1a6a906ba62f2556372282b1ef37b26bca67e3d2ea957277cfcefc6275cca7", size = 1989501, upload-time = "2025-04-03T20:36:13.43Z" }, + { url = "https://files.pythonhosted.org/packages/b7/53/1f7eb7ee83a06c400089ec7cb841cbd581c2edd7a4b21eb2f31030b88daa/rapidfuzz-3.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2fd0975e015b05c79a97f38883a11236f5a24cca83aa992bd2558ceaa5652b26", size = 1445379, upload-time = "2025-04-03T20:36:16.439Z" }, + { url = "https://files.pythonhosted.org/packages/07/09/de8069a4599cc8e6d194e5fa1782c561151dea7d5e2741767137e2a8c1f0/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d4e13593d298c50c4f94ce453f757b4b398af3fa0fd2fde693c3e51195b7f69", size = 1405986, upload-time = "2025-04-03T20:36:18.447Z" }, + { url = "https://files.pythonhosted.org/packages/5d/77/d9a90b39c16eca20d70fec4ca377fbe9ea4c0d358c6e4736ab0e0e78aaf6/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed6f416bda1c9133000009d84d9409823eb2358df0950231cc936e4bf784eb97", size = 5310809, upload-time = "2025-04-03T20:36:20.324Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7d/14da291b0d0f22262d19522afaf63bccf39fc027c981233fb2137a57b71f/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1dc82b6ed01acb536b94a43996a94471a218f4d89f3fdd9185ab496de4b2a981", size = 1629394, upload-time = "2025-04-03T20:36:22.256Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e4/79ed7e4fa58f37c0f8b7c0a62361f7089b221fe85738ae2dbcfb815e985a/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9d824de871daa6e443b39ff495a884931970d567eb0dfa213d234337343835f", size = 1600544, upload-time = "2025-04-03T20:36:24.207Z" }, + { url = "https://files.pythonhosted.org/packages/4e/20/e62b4d13ba851b0f36370060025de50a264d625f6b4c32899085ed51f980/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d18228a2390375cf45726ce1af9d36ff3dc1f11dce9775eae1f1b13ac6ec50f", size = 3052796, upload-time = "2025-04-03T20:36:26.279Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8d/55fdf4387dec10aa177fe3df8dbb0d5022224d95f48664a21d6b62a5299d/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5fe634c9482ec5d4a6692afb8c45d370ae86755e5f57aa6c50bfe4ca2bdd87", size = 2464016, upload-time = "2025-04-03T20:36:28.525Z" }, + { url = "https://files.pythonhosted.org/packages/9b/be/0872f6a56c0f473165d3b47d4170fa75263dc5f46985755aa9bf2bbcdea1/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:694eb531889f71022b2be86f625a4209c4049e74be9ca836919b9e395d5e33b3", size = 7556725, upload-time = "2025-04-03T20:36:30.629Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f3/6c0750e484d885a14840c7a150926f425d524982aca989cdda0bb3bdfa57/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:11b47b40650e06147dee5e51a9c9ad73bb7b86968b6f7d30e503b9f8dd1292db", size = 2859052, upload-time = "2025-04-03T20:36:32.836Z" }, + { url = "https://files.pythonhosted.org/packages/6f/98/5a3a14701b5eb330f444f7883c9840b43fb29c575e292e09c90a270a6e07/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:98b8107ff14f5af0243f27d236bcc6e1ef8e7e3b3c25df114e91e3a99572da73", size = 3390219, upload-time = "2025-04-03T20:36:35.062Z" }, + { url = "https://files.pythonhosted.org/packages/e9/7d/f4642eaaeb474b19974332f2a58471803448be843033e5740965775760a5/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b836f486dba0aceb2551e838ff3f514a38ee72b015364f739e526d720fdb823a", size = 4377924, upload-time = "2025-04-03T20:36:37.363Z" }, + { url = "https://files.pythonhosted.org/packages/8e/83/fa33f61796731891c3e045d0cbca4436a5c436a170e7f04d42c2423652c3/rapidfuzz-3.13.0-cp312-cp312-win32.whl", hash = "sha256:4671ee300d1818d7bdfd8fa0608580d7778ba701817216f0c17fb29e6b972514", size = 1823915, upload-time = "2025-04-03T20:36:39.451Z" }, + { url = "https://files.pythonhosted.org/packages/03/25/5ee7ab6841ca668567d0897905eebc79c76f6297b73bf05957be887e9c74/rapidfuzz-3.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e2065f68fb1d0bf65adc289c1bdc45ba7e464e406b319d67bb54441a1b9da9e", size = 1616985, upload-time = "2025-04-03T20:36:41.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/5e/3f0fb88db396cb692aefd631e4805854e02120a2382723b90dcae720bcc6/rapidfuzz-3.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:65cc97c2fc2c2fe23586599686f3b1ceeedeca8e598cfcc1b7e56dc8ca7e2aa7", size = 860116, upload-time = "2025-04-03T20:36:43.915Z" }, + { url = "https://files.pythonhosted.org/packages/88/df/6060c5a9c879b302bd47a73fc012d0db37abf6544c57591bcbc3459673bd/rapidfuzz-3.13.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1ba007f4d35a45ee68656b2eb83b8715e11d0f90e5b9f02d615a8a321ff00c27", size = 1905935, upload-time = "2025-04-03T20:38:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/a2/6c/a0b819b829e20525ef1bd58fc776fb8d07a0c38d819e63ba2b7c311a2ed4/rapidfuzz-3.13.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d7a217310429b43be95b3b8ad7f8fc41aba341109dc91e978cd7c703f928c58f", size = 1383714, upload-time = "2025-04-03T20:38:20.628Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c1/3da3466cc8a9bfb9cd345ad221fac311143b6a9664b5af4adb95b5e6ce01/rapidfuzz-3.13.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:558bf526bcd777de32b7885790a95a9548ffdcce68f704a81207be4a286c1095", size = 1367329, upload-time = "2025-04-03T20:38:23.01Z" }, + { url = "https://files.pythonhosted.org/packages/da/f0/9f2a9043bfc4e66da256b15d728c5fc2d865edf0028824337f5edac36783/rapidfuzz-3.13.0-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:202a87760f5145140d56153b193a797ae9338f7939eb16652dd7ff96f8faf64c", size = 5251057, upload-time = "2025-04-03T20:38:25.52Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ff/af2cb1d8acf9777d52487af5c6b34ce9d13381a753f991d95ecaca813407/rapidfuzz-3.13.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfcccc08f671646ccb1e413c773bb92e7bba789e3a1796fd49d23c12539fe2e4", size = 2992401, upload-time = "2025-04-03T20:38:28.196Z" }, + { url = "https://files.pythonhosted.org/packages/c1/c5/c243b05a15a27b946180db0d1e4c999bef3f4221505dff9748f1f6c917be/rapidfuzz-3.13.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:1f219f1e3c3194d7a7de222f54450ce12bc907862ff9a8962d83061c1f923c86", size = 1553782, upload-time = "2025-04-03T20:38:30.778Z" }, ] [[package]] From 6b579aa7d9d4532922cff090f5d6a3c360d49aab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Cartagena?= Date: Sun, 19 Jul 2026 16:54:14 +0200 Subject: [PATCH 3/7] fix(interview): raise batch interview timeout from 180s to 600s MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit interview_agents() (used by the report generator to interview the top N most-active agents post-simulation) sends a batch IPC command with a hardcoded 180s timeout covering up to N agents x 2 platforms of individual LLM interview calls. With CLI-based LLM providers this was consistently timing out with 0/N successful interviews on runs with 100+ agent profiles, even though the simulation itself completed normally — the report would silently fall back to "(no interview data)" since the failure is caught as non-fatal by the caller. 180s doesn't leave enough headroom when each individual call can legitimately take up to the LLM client's own per-call ceiling (600s in llm_client.py, e.g. for claude-cli subprocess spawns). Raised to match that ceiling. --- app/services/graph_tools.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/services/graph_tools.py b/app/services/graph_tools.py index 92d3b62..291df71 100644 --- a/app/services/graph_tools.py +++ b/app/services/graph_tools.py @@ -352,7 +352,14 @@ def interview_agents( simulation_id=simulation_id, interviews=interviews_request, platform=None, - timeout=180.0, + # 180s was too tight for CLI-based LLM providers (e.g. claude-cli): + # each interview response is a subprocess spawn that can legitimately + # take up to the LLM client's own per-call ceiling (600s in + # llm_client.py). A batch of up to len(selected_indices) agents x 2 + # platforms was routinely timing out at 0/N successful interviews + # even on runs with 100+ agent profiles. Raised to match that ceiling + # with headroom for a few sequential/parallel calls. + timeout=600.0, ) if not api_result.get("success", False): From 24b4a587fe122c553296102454ef72a213e0961f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Cartagena?= Date: Sun, 19 Jul 2026 18:12:45 +0200 Subject: [PATCH 4/7] fix(review): harden CLI provider handling per review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses self-review findings on the original three commits: - Single CLI_CALL_TIMEOUT_SECONDS constant governs both providers and every timeout error message (the claude handler still said "300s" after the ceiling moved to 600; codex was left at 180 with no encoding handling — same cp1252 hazards the claude path already fixed). - Interview batch budget now scales with the batch instead of a flat 600s: one full per-call ceiling per interviewed agent. A fixed budget equals the worst case of a single call and still starves multi-agent batches. - rapidfuzz override scoped with sys_platform == 'win32' marker so macOS/Linux keep the resolver's pick; pyproject now documents why the pin exists and when to drop it (the missing wheels are specifically Windows cp312, not all of win_amd64). - Provider binaries resolved via shutil.which for a clear early error when the CLI is missing (and to tolerate npm-style .cmd shims on Windows). - Warn when undecodable bytes were replaced with U+FFFD instead of corrupting output silently. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- app/services/graph_tools.py | 14 ++++++------- app/utils/llm_client.py | 42 +++++++++++++++++++++++++++++++------ pyproject.toml | 5 ++++- uv.lock | 33 ++--------------------------- 4 files changed, 48 insertions(+), 46 deletions(-) diff --git a/app/services/graph_tools.py b/app/services/graph_tools.py index 291df71..afa411a 100644 --- a/app/services/graph_tools.py +++ b/app/services/graph_tools.py @@ -299,6 +299,7 @@ def interview_agents( ) -> InterviewResult: """Interview simulated agents via OASIS IPC.""" from .simulation_runner import SimulationRunner + from ..utils.llm_client import CLI_CALL_TIMEOUT_SECONDS result = InterviewResult( interview_topic=interview_requirement, @@ -348,18 +349,15 @@ def interview_agents( for idx in selected_indices ] + # One full CLI-call ceiling per interviewed agent: a fixed 180s budget + # starved whole batches to 0/N with subprocess-based providers. + batch_timeout = float(CLI_CALL_TIMEOUT_SECONDS * max(1, len(interviews_request))) + api_result = SimulationRunner.interview_agents_batch( simulation_id=simulation_id, interviews=interviews_request, platform=None, - # 180s was too tight for CLI-based LLM providers (e.g. claude-cli): - # each interview response is a subprocess spawn that can legitimately - # take up to the LLM client's own per-call ceiling (600s in - # llm_client.py). A batch of up to len(selected_indices) agents x 2 - # platforms was routinely timing out at 0/N successful interviews - # even on runs with 100+ agent profiles. Raised to match that ceiling - # with headroom for a few sequential/parallel calls. - timeout=600.0, + timeout=batch_timeout, ) if not api_result.get("success", False): diff --git a/app/utils/llm_client.py b/app/utils/llm_client.py index a417d40..78c0ff6 100644 --- a/app/utils/llm_client.py +++ b/app/utils/llm_client.py @@ -4,6 +4,7 @@ import json import re +import shutil import subprocess import time import tempfile @@ -17,6 +18,28 @@ MAX_RETRIES = 3 RETRY_BASE_DELAY = 2.0 # seconds +# Wall-clock ceiling for a single CLI provider call. Long prompts (large +# knowledge-graph context, interview batches) can legitimately take several +# minutes through a subprocess-based provider. Callers that wait on multiple +# sequential calls (e.g. interview batches) should budget per call using this +# constant rather than hardcoding their own number. +CLI_CALL_TIMEOUT_SECONDS = 600 + + +def _resolve_cli(binary: str) -> str: + """Resolve a provider CLI to an absolute path, failing with a clear error. + + On Windows, invoking a bare name only works for .exe; npm-style .cmd shims + need the resolved path. shutil.which also gives us a clean early failure + instead of an opaque FileNotFoundError mid-simulation. + """ + resolved = shutil.which(binary) + if resolved is None: + raise RuntimeError( + f"'{binary}' CLI not found on PATH. Install it and log in before running simulations." + ) + return resolved + class LLMClient: """LLM Client — supports claude-cli and codex-cli.""" @@ -93,9 +116,9 @@ def _chat_claude_cli( try: result = subprocess.run( - ["claude", "-p", "--output-format", "json"], + [_resolve_cli("claude"), "-p", "--output-format", "json"], input=prompt, - capture_output=True, text=True, timeout=600, + capture_output=True, text=True, timeout=CLI_CALL_TIMEOUT_SECONDS, encoding="utf-8", errors="replace", cwd=tempfile.gettempdir() ) @@ -104,6 +127,9 @@ def _chat_claude_cli( logger.error(f"Claude CLI error: {result.stderr[:200]}") raise RuntimeError(f"Claude CLI failed: {result.stderr[:200]}") + if "�" in result.stdout: + logger.warning("Claude CLI output contained undecodable bytes; replaced with U+FFFD") + try: output = json.loads(result.stdout) content = output.get("result", result.stdout) @@ -113,7 +139,7 @@ def _chat_claude_cli( return self._clean_content(content) except subprocess.TimeoutExpired: - raise RuntimeError("Claude CLI timed out after 300s") + raise RuntimeError(f"Claude CLI timed out after {CLI_CALL_TIMEOUT_SECONDS}s") def _chat_codex_cli( self, @@ -140,9 +166,10 @@ def _chat_codex_cli( try: result = subprocess.run( - ["codex", "exec", "--skip-git-repo-check"], + [_resolve_cli("codex"), "exec", "--skip-git-repo-check"], input=prompt, - capture_output=True, text=True, timeout=180, + capture_output=True, text=True, timeout=CLI_CALL_TIMEOUT_SECONDS, + encoding="utf-8", errors="replace", cwd=tempfile.gettempdir() ) @@ -150,6 +177,9 @@ def _chat_codex_cli( logger.error(f"Codex CLI error: {result.stderr[:200]}") raise RuntimeError(f"Codex CLI failed: {result.stderr[:200]}") + if "�" in result.stdout: + logger.warning("Codex CLI output contained undecodable bytes; replaced with U+FFFD") + raw = result.stdout.strip() parts = raw.split("\ncodex\n") if len(parts) > 1: @@ -166,7 +196,7 @@ def _chat_codex_cli( return self._clean_content(content) except subprocess.TimeoutExpired: - raise RuntimeError("Codex CLI timed out after 180s") + raise RuntimeError(f"Codex CLI timed out after {CLI_CALL_TIMEOUT_SECONDS}s") def chat_json( self, diff --git a/pyproject.toml b/pyproject.toml index f015395..52aca74 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,4 +45,7 @@ dev = [ packages = ["app"] [tool.uv] -override-dependencies = ["rapidfuzz==3.13.0"] +# rapidfuzz >=3.14 ships no Windows cp312 wheels (and no sdist in-range), which +# breaks `uv sync` under requires-python <3.13 on Windows. Scoped to win32 so +# macOS/Linux keep the resolver's pick; drop once upstream publishes wheels. +override-dependencies = ["rapidfuzz==3.13.0; sys_platform == 'win32'"] diff --git a/uv.lock b/uv.lock index a936512..b2d0726 100644 --- a/uv.lock +++ b/uv.lock @@ -7,7 +7,7 @@ resolution-markers = [ ] [manifest] -overrides = [{ name = "rapidfuzz", specifier = "==3.13.0" }] +overrides = [{ name = "rapidfuzz", marker = "sys_platform == 'win32'", specifier = "==3.13.0" }] [[package]] name = "aiofiles" @@ -1663,41 +1663,12 @@ version = "3.13.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ed/f6/6895abc3a3d056b9698da3199b04c0e56226d530ae44a470edabf8b664f0/rapidfuzz-3.13.0.tar.gz", hash = "sha256:d2eaf3839e52cbcc0accbe9817a67b4b0fcf70aaeb229cfddc1c28061f9ce5d8", size = 57904226, upload-time = "2025-04-03T20:38:51.226Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/17/9be9eff5a3c7dfc831c2511262082c6786dca2ce21aa8194eef1cb71d67a/rapidfuzz-3.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d395a5cad0c09c7f096433e5fd4224d83b53298d53499945a9b0e5a971a84f3a", size = 1999453, upload-time = "2025-04-03T20:35:40.804Z" }, - { url = "https://files.pythonhosted.org/packages/75/67/62e57896ecbabe363f027d24cc769d55dd49019e576533ec10e492fcd8a2/rapidfuzz-3.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7b3eda607a019169f7187328a8d1648fb9a90265087f6903d7ee3a8eee01805", size = 1450881, upload-time = "2025-04-03T20:35:42.734Z" }, - { url = "https://files.pythonhosted.org/packages/96/5c/691c5304857f3476a7b3df99e91efc32428cbe7d25d234e967cc08346c13/rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98e0bfa602e1942d542de077baf15d658bd9d5dcfe9b762aff791724c1c38b70", size = 1422990, upload-time = "2025-04-03T20:35:45.158Z" }, - { url = "https://files.pythonhosted.org/packages/46/81/7a7e78f977496ee2d613154b86b203d373376bcaae5de7bde92f3ad5a192/rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bef86df6d59667d9655905b02770a0c776d2853971c0773767d5ef8077acd624", size = 5342309, upload-time = "2025-04-03T20:35:46.952Z" }, - { url = "https://files.pythonhosted.org/packages/51/44/12fdd12a76b190fe94bf38d252bb28ddf0ab7a366b943e792803502901a2/rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fedd316c165beed6307bf754dee54d3faca2c47e1f3bcbd67595001dfa11e969", size = 1656881, upload-time = "2025-04-03T20:35:49.954Z" }, - { url = "https://files.pythonhosted.org/packages/27/ae/0d933e660c06fcfb087a0d2492f98322f9348a28b2cc3791a5dbadf6e6fb/rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5158da7f2ec02a930be13bac53bb5903527c073c90ee37804090614cab83c29e", size = 1608494, upload-time = "2025-04-03T20:35:51.646Z" }, - { url = "https://files.pythonhosted.org/packages/3d/2c/4b2f8aafdf9400e5599b6ed2f14bc26ca75f5a923571926ccbc998d4246a/rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b6f913ee4618ddb6d6f3e387b76e8ec2fc5efee313a128809fbd44e65c2bbb2", size = 3072160, upload-time = "2025-04-03T20:35:53.472Z" }, - { url = "https://files.pythonhosted.org/packages/60/7d/030d68d9a653c301114101c3003b31ce01cf2c3224034cd26105224cd249/rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d25fdbce6459ccbbbf23b4b044f56fbd1158b97ac50994eaae2a1c0baae78301", size = 2491549, upload-time = "2025-04-03T20:35:55.391Z" }, - { url = "https://files.pythonhosted.org/packages/8e/cd/7040ba538fc6a8ddc8816a05ecf46af9988b46c148ddd7f74fb0fb73d012/rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:25343ccc589a4579fbde832e6a1e27258bfdd7f2eb0f28cb836d6694ab8591fc", size = 7584142, upload-time = "2025-04-03T20:35:57.71Z" }, - { url = "https://files.pythonhosted.org/packages/c1/96/85f7536fbceb0aa92c04a1c37a3fc4fcd4e80649e9ed0fb585382df82edc/rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a9ad1f37894e3ffb76bbab76256e8a8b789657183870be11aa64e306bb5228fd", size = 2896234, upload-time = "2025-04-03T20:35:59.969Z" }, - { url = "https://files.pythonhosted.org/packages/55/fd/460e78438e7019f2462fe9d4ecc880577ba340df7974c8a4cfe8d8d029df/rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5dc71ef23845bb6b62d194c39a97bb30ff171389c9812d83030c1199f319098c", size = 3437420, upload-time = "2025-04-03T20:36:01.91Z" }, - { url = "https://files.pythonhosted.org/packages/cc/df/c3c308a106a0993befd140a414c5ea78789d201cf1dfffb8fd9749718d4f/rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b7f4c65facdb94f44be759bbd9b6dda1fa54d0d6169cdf1a209a5ab97d311a75", size = 4410860, upload-time = "2025-04-03T20:36:04.352Z" }, { url = "https://files.pythonhosted.org/packages/75/ee/9d4ece247f9b26936cdeaae600e494af587ce9bf8ddc47d88435f05cfd05/rapidfuzz-3.13.0-cp311-cp311-win32.whl", hash = "sha256:b5104b62711565e0ff6deab2a8f5dbf1fbe333c5155abe26d2cfd6f1849b6c87", size = 1843161, upload-time = "2025-04-03T20:36:06.802Z" }, { url = "https://files.pythonhosted.org/packages/c9/5a/d00e1f63564050a20279015acb29ecaf41646adfacc6ce2e1e450f7f2633/rapidfuzz-3.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:9093cdeb926deb32a4887ebe6910f57fbcdbc9fbfa52252c10b56ef2efb0289f", size = 1629962, upload-time = "2025-04-03T20:36:09.133Z" }, { url = "https://files.pythonhosted.org/packages/3b/74/0a3de18bc2576b794f41ccd07720b623e840fda219ab57091897f2320fdd/rapidfuzz-3.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:f70f646751b6aa9d05be1fb40372f006cc89d6aad54e9d79ae97bd1f5fce5203", size = 866631, upload-time = "2025-04-03T20:36:11.022Z" }, - { url = "https://files.pythonhosted.org/packages/13/4b/a326f57a4efed8f5505b25102797a58e37ee11d94afd9d9422cb7c76117e/rapidfuzz-3.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a1a6a906ba62f2556372282b1ef37b26bca67e3d2ea957277cfcefc6275cca7", size = 1989501, upload-time = "2025-04-03T20:36:13.43Z" }, - { url = "https://files.pythonhosted.org/packages/b7/53/1f7eb7ee83a06c400089ec7cb841cbd581c2edd7a4b21eb2f31030b88daa/rapidfuzz-3.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2fd0975e015b05c79a97f38883a11236f5a24cca83aa992bd2558ceaa5652b26", size = 1445379, upload-time = "2025-04-03T20:36:16.439Z" }, - { url = "https://files.pythonhosted.org/packages/07/09/de8069a4599cc8e6d194e5fa1782c561151dea7d5e2741767137e2a8c1f0/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d4e13593d298c50c4f94ce453f757b4b398af3fa0fd2fde693c3e51195b7f69", size = 1405986, upload-time = "2025-04-03T20:36:18.447Z" }, - { url = "https://files.pythonhosted.org/packages/5d/77/d9a90b39c16eca20d70fec4ca377fbe9ea4c0d358c6e4736ab0e0e78aaf6/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed6f416bda1c9133000009d84d9409823eb2358df0950231cc936e4bf784eb97", size = 5310809, upload-time = "2025-04-03T20:36:20.324Z" }, - { url = "https://files.pythonhosted.org/packages/1e/7d/14da291b0d0f22262d19522afaf63bccf39fc027c981233fb2137a57b71f/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1dc82b6ed01acb536b94a43996a94471a218f4d89f3fdd9185ab496de4b2a981", size = 1629394, upload-time = "2025-04-03T20:36:22.256Z" }, - { url = "https://files.pythonhosted.org/packages/b7/e4/79ed7e4fa58f37c0f8b7c0a62361f7089b221fe85738ae2dbcfb815e985a/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9d824de871daa6e443b39ff495a884931970d567eb0dfa213d234337343835f", size = 1600544, upload-time = "2025-04-03T20:36:24.207Z" }, - { url = "https://files.pythonhosted.org/packages/4e/20/e62b4d13ba851b0f36370060025de50a264d625f6b4c32899085ed51f980/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d18228a2390375cf45726ce1af9d36ff3dc1f11dce9775eae1f1b13ac6ec50f", size = 3052796, upload-time = "2025-04-03T20:36:26.279Z" }, - { url = "https://files.pythonhosted.org/packages/cd/8d/55fdf4387dec10aa177fe3df8dbb0d5022224d95f48664a21d6b62a5299d/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5fe634c9482ec5d4a6692afb8c45d370ae86755e5f57aa6c50bfe4ca2bdd87", size = 2464016, upload-time = "2025-04-03T20:36:28.525Z" }, - { url = "https://files.pythonhosted.org/packages/9b/be/0872f6a56c0f473165d3b47d4170fa75263dc5f46985755aa9bf2bbcdea1/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:694eb531889f71022b2be86f625a4209c4049e74be9ca836919b9e395d5e33b3", size = 7556725, upload-time = "2025-04-03T20:36:30.629Z" }, - { url = "https://files.pythonhosted.org/packages/5d/f3/6c0750e484d885a14840c7a150926f425d524982aca989cdda0bb3bdfa57/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:11b47b40650e06147dee5e51a9c9ad73bb7b86968b6f7d30e503b9f8dd1292db", size = 2859052, upload-time = "2025-04-03T20:36:32.836Z" }, - { url = "https://files.pythonhosted.org/packages/6f/98/5a3a14701b5eb330f444f7883c9840b43fb29c575e292e09c90a270a6e07/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:98b8107ff14f5af0243f27d236bcc6e1ef8e7e3b3c25df114e91e3a99572da73", size = 3390219, upload-time = "2025-04-03T20:36:35.062Z" }, - { url = "https://files.pythonhosted.org/packages/e9/7d/f4642eaaeb474b19974332f2a58471803448be843033e5740965775760a5/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b836f486dba0aceb2551e838ff3f514a38ee72b015364f739e526d720fdb823a", size = 4377924, upload-time = "2025-04-03T20:36:37.363Z" }, { url = "https://files.pythonhosted.org/packages/8e/83/fa33f61796731891c3e045d0cbca4436a5c436a170e7f04d42c2423652c3/rapidfuzz-3.13.0-cp312-cp312-win32.whl", hash = "sha256:4671ee300d1818d7bdfd8fa0608580d7778ba701817216f0c17fb29e6b972514", size = 1823915, upload-time = "2025-04-03T20:36:39.451Z" }, { url = "https://files.pythonhosted.org/packages/03/25/5ee7ab6841ca668567d0897905eebc79c76f6297b73bf05957be887e9c74/rapidfuzz-3.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e2065f68fb1d0bf65adc289c1bdc45ba7e464e406b319d67bb54441a1b9da9e", size = 1616985, upload-time = "2025-04-03T20:36:41.631Z" }, { url = "https://files.pythonhosted.org/packages/76/5e/3f0fb88db396cb692aefd631e4805854e02120a2382723b90dcae720bcc6/rapidfuzz-3.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:65cc97c2fc2c2fe23586599686f3b1ceeedeca8e598cfcc1b7e56dc8ca7e2aa7", size = 860116, upload-time = "2025-04-03T20:36:43.915Z" }, - { url = "https://files.pythonhosted.org/packages/88/df/6060c5a9c879b302bd47a73fc012d0db37abf6544c57591bcbc3459673bd/rapidfuzz-3.13.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1ba007f4d35a45ee68656b2eb83b8715e11d0f90e5b9f02d615a8a321ff00c27", size = 1905935, upload-time = "2025-04-03T20:38:18.07Z" }, - { url = "https://files.pythonhosted.org/packages/a2/6c/a0b819b829e20525ef1bd58fc776fb8d07a0c38d819e63ba2b7c311a2ed4/rapidfuzz-3.13.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d7a217310429b43be95b3b8ad7f8fc41aba341109dc91e978cd7c703f928c58f", size = 1383714, upload-time = "2025-04-03T20:38:20.628Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c1/3da3466cc8a9bfb9cd345ad221fac311143b6a9664b5af4adb95b5e6ce01/rapidfuzz-3.13.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:558bf526bcd777de32b7885790a95a9548ffdcce68f704a81207be4a286c1095", size = 1367329, upload-time = "2025-04-03T20:38:23.01Z" }, - { url = "https://files.pythonhosted.org/packages/da/f0/9f2a9043bfc4e66da256b15d728c5fc2d865edf0028824337f5edac36783/rapidfuzz-3.13.0-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:202a87760f5145140d56153b193a797ae9338f7939eb16652dd7ff96f8faf64c", size = 5251057, upload-time = "2025-04-03T20:38:25.52Z" }, - { url = "https://files.pythonhosted.org/packages/6a/ff/af2cb1d8acf9777d52487af5c6b34ce9d13381a753f991d95ecaca813407/rapidfuzz-3.13.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfcccc08f671646ccb1e413c773bb92e7bba789e3a1796fd49d23c12539fe2e4", size = 2992401, upload-time = "2025-04-03T20:38:28.196Z" }, { url = "https://files.pythonhosted.org/packages/c1/c5/c243b05a15a27b946180db0d1e4c999bef3f4221505dff9748f1f6c917be/rapidfuzz-3.13.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:1f219f1e3c3194d7a7de222f54450ce12bc907862ff9a8962d83061c1f923c86", size = 1553782, upload-time = "2025-04-03T20:38:30.778Z" }, ] @@ -2274,7 +2245,7 @@ dependencies = [ { name = "numpy" }, { name = "python-iso639" }, { name = "python-magic" }, - { name = "rapidfuzz" }, + { name = "rapidfuzz", marker = "sys_platform == 'win32'" }, { name = "requests" }, { name = "tabulate" }, { name = "typing-extensions" }, From 0b240304fd4c89f24619a59fa5dcb20b4a1fdbee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Cartagena?= Date: Sun, 19 Jul 2026 18:44:28 +0200 Subject: [PATCH 5/7] feat(interviews): configurable panel size with stratified sampling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The post-simulation interview panel was hardcoded to the 5 most active agents, which (a) caps the qualitative evidence the final report can draw on and (b) over-represents the vocal minority — silent agents, who watched the whole simulation and engaged with none of it, often hold the most informative objections. - MIROFISH_INTERVIEW_MAX_AGENTS env var (default 5, preserving current behavior) controls panel size. Cost scales linearly: one LLM call per agent per platform; the batch timeout already scales with panel size. - With activity data and a panel of >=6, selection becomes stratified: ~40% most vocal, ~30% LLM-picked for topical relevance from the remainder, ~30% silent non-participants sampled across the roster. Below the threshold the existing LLM selection path is unchanged. - MIROFISH_INTERVIEW_EXTRA_QUESTIONS (';'-separated) appends fixed questions to the generated questionnaire — closed questions (price bands, adoption timing) whose answers can be coded into distributions for downstream quantitative use. Tested: stratified selector unit-smoke (quotas, no duplicates, range, classic path below threshold, no-LLM fallback) plus py_compile. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- app/config.py | 9 ++++++ app/services/graph_tools.py | 53 ++++++++++++++++++++++++++++++++++-- app/services/report_agent.py | 14 +++++++--- 3 files changed, 70 insertions(+), 6 deletions(-) diff --git a/app/config.py b/app/config.py index ef3effd..cc1a6a7 100644 --- a/app/config.py +++ b/app/config.py @@ -40,6 +40,15 @@ class Config: LLM_PROVIDER = os.environ.get("LLM_PROVIDER", "claude-cli").strip().lower() + # Post-simulation interviews for the final report. Each interviewed agent + # costs one LLM call per platform, so the default stays small; raise it to + # build a larger synthetic panel (>=6 also enables stratified sampling of + # vocal / relevant / silent agents). EXTRA_QUESTIONS (';'-separated) are + # appended to the generated questionnaire — useful for closed questions + # whose answers can be coded into distributions. + INTERVIEW_MAX_AGENTS = int(os.environ.get("MIROFISH_INTERVIEW_MAX_AGENTS", "5")) + INTERVIEW_EXTRA_QUESTIONS = os.environ.get("MIROFISH_INTERVIEW_EXTRA_QUESTIONS", "").strip() + DATA_DIR = _resolve_path(os.path.join(os.path.dirname(__file__), "../data/graphs"), "DATA_DIR") MAX_CONTENT_LENGTH = 50 * 1024 * 1024 diff --git a/app/services/graph_tools.py b/app/services/graph_tools.py index afa411a..4e7ad20 100644 --- a/app/services/graph_tools.py +++ b/app/services/graph_tools.py @@ -11,6 +11,7 @@ from .graph_db import GraphDatabase from .graph_storage import GraphStorage +from ..config import Config from ..utils.logger import get_logger from ..utils.llm_client import LLMClient @@ -296,8 +297,14 @@ def interview_agents( simulation_requirement: str = "", max_agents: int = 5, custom_questions: List[str] = None, + activity_by_agent: Optional[Dict[int, int]] = None, ) -> InterviewResult: - """Interview simulated agents via OASIS IPC.""" + """Interview simulated agents via OASIS IPC. + + activity_by_agent maps agent index -> action count; when provided (and + the panel is large enough) it enables stratified sampling so the panel + mixes vocal, relevant and silent agents instead of only top posters. + """ from .simulation_runner import SimulationRunner from ..utils.llm_client import CLI_CALL_TIMEOUT_SECONDS @@ -318,6 +325,7 @@ def interview_agents( interview_requirement=interview_requirement, simulation_requirement=simulation_requirement, max_agents=max_agents, + activity_by_agent=activity_by_agent, ) result.selected_agents = selected_agents result.selection_reasoning = selection_reasoning @@ -329,6 +337,11 @@ def interview_agents( selected_agents=selected_agents, ) + if Config.INTERVIEW_EXTRA_QUESTIONS: + result.interview_questions.extend( + q.strip() for q in Config.INTERVIEW_EXTRA_QUESTIONS.split(";") if q.strip() + ) + combined_prompt = "\n".join([f"{i+1}. {q}" for i, q in enumerate(result.interview_questions)]) optimized_prompt = ( "You are being interviewed. Please draw on your persona, all past memories, and actions " @@ -478,7 +491,43 @@ def _load_agent_profiles(self, simulation_id: str) -> List[Dict[str, Any]]: return [] - def _select_agents_for_interview(self, profiles, interview_requirement, simulation_requirement, max_agents): + def _select_agents_for_interview(self, profiles, interview_requirement, simulation_requirement, max_agents, + activity_by_agent=None): + # Stratified sampling for larger panels: pure top-poster selection + # over-represents the vocal minority, while silent agents often hold + # the most informative objections (they saw everything and engaged + # with none of it). Quotas: ~40% most vocal, ~30% LLM-picked for + # relevance from the remainder, ~30% silent non-participants. + if activity_by_agent and max_agents >= 6 and len(profiles) > max_agents: + n_vocal = max(1, round(max_agents * 0.4)) + n_silent = max(1, round(max_agents * 0.3)) + n_llm = max(0, max_agents - n_vocal - n_silent) + + by_activity = sorted(range(len(profiles)), key=lambda i: -activity_by_agent.get(i, 0)) + vocal = [i for i in by_activity if activity_by_agent.get(i, 0) > 0][:n_vocal] + silent_pool = [i for i in range(len(profiles)) + if activity_by_agent.get(i, 0) == 0 and i not in vocal] + stride = max(1, len(silent_pool) // n_silent) if silent_pool else 1 + silent = silent_pool[::stride][:n_silent] + + chosen = set(vocal) | set(silent) + remainder_idx = [i for i in range(len(profiles)) if i not in chosen] + llm_sel = [] + if n_llm > 0 and remainder_idx: + remainder = [profiles[i] for i in remainder_idx] + _, rel_idx, _ = self._select_agents_for_interview( + remainder, interview_requirement, simulation_requirement, n_llm + ) + llm_sel = [remainder_idx[i] for i in rel_idx] + + indices = vocal + llm_sel + silent + agents = [profiles[i] for i in indices] + reasoning = ( + f"Stratified panel of {len(indices)}: {len(vocal)} most vocal, " + f"{len(llm_sel)} LLM-picked for relevance, {len(silent)} silent non-participants" + ) + return agents, indices, reasoning + agent_summaries = [ { "index": i, diff --git a/app/services/report_agent.py b/app/services/report_agent.py index ff6ae32..adc9ce4 100644 --- a/app/services/report_agent.py +++ b/app/services/report_agent.py @@ -185,16 +185,22 @@ def generate_report_fast( f"[Round {a.round_num}] [{a.platform}] {a.agent_name} ({a.action_type}): {content_preview}" ) - # Interview top agents + # Interview a panel of agents (size configurable; with activity + # data the selector stratifies vocal / relevant / silent agents) interview_text = "" try: - top_agents = sorted(agent_stats, key=lambda x: x.get("total_actions", 0), reverse=True)[:5] - if top_agents: + if agent_stats: + activity_by_agent = { + int(s["agent_id"]): int(s.get("total_actions", 0)) + for s in agent_stats + if s.get("agent_id") is not None + } interview_result = self.graph_tools.interview_agents( simulation_id=self.simulation_id, interview_requirement=f"What is your prediction for how {self.simulation_requirement}? What surprised you during the simulation?", simulation_requirement=self.simulation_requirement, - max_agents=5, + max_agents=Config.INTERVIEW_MAX_AGENTS, + activity_by_agent=activity_by_agent, ) if hasattr(interview_result, "to_text"): interview_text = interview_result.to_text() From 6f51c5234c4971bbed7713b848235c4518354c60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Cartagena?= Date: Sun, 19 Jul 2026 21:07:56 +0200 Subject: [PATCH 6/7] fix(review): no-retry timeouts and cached CLI resolution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LLMTimeoutError (RuntimeError subclass) raised on TimeoutExpired and excluded from the x3 retry loop: a prompt that exhausts the 600s ceiling will almost certainly exhaust it again, so retrying turned one slow call into ~30 minutes of blocked wall clock. - _resolve_cli cached with lru_cache: the binary does not move during the process lifetime, so the per-request PATH scan was waste. (Failures are not cached: lru_cache does not memoize exceptions.) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- app/utils/llm_client.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/app/utils/llm_client.py b/app/utils/llm_client.py index 78c0ff6..bf08d2e 100644 --- a/app/utils/llm_client.py +++ b/app/utils/llm_client.py @@ -2,6 +2,7 @@ LLM Client — CLI-only providers (Claude Code, Codex). """ +import functools import json import re import shutil @@ -26,6 +27,17 @@ CLI_CALL_TIMEOUT_SECONDS = 600 +class LLMTimeoutError(RuntimeError): + """A CLI call exhausted its wall-clock ceiling. + + Kept distinct from generic RuntimeError so the retry loop can skip it: + a prompt that exhausts the full ceiling will almost certainly exhaust it + again, and retrying only multiplies the wait (3x600s + backoff ~= 30 min + blocked on a single call). + """ + + +@functools.lru_cache(maxsize=None) def _resolve_cli(binary: str) -> str: """Resolve a provider CLI to an absolute path, failing with a clear error. @@ -83,6 +95,9 @@ def chat( if self.provider == "codex-cli": return self._chat_codex_cli(messages, temperature, max_tokens, response_format) return self._chat_claude_cli(messages, temperature, max_tokens, response_format) + except LLMTimeoutError: + # No retry: see LLMTimeoutError docstring + raise except RuntimeError as exc: last_error = exc if attempt < MAX_RETRIES - 1: @@ -139,7 +154,7 @@ def _chat_claude_cli( return self._clean_content(content) except subprocess.TimeoutExpired: - raise RuntimeError(f"Claude CLI timed out after {CLI_CALL_TIMEOUT_SECONDS}s") + raise LLMTimeoutError(f"Claude CLI timed out after {CLI_CALL_TIMEOUT_SECONDS}s") def _chat_codex_cli( self, @@ -196,7 +211,7 @@ def _chat_codex_cli( return self._clean_content(content) except subprocess.TimeoutExpired: - raise RuntimeError(f"Codex CLI timed out after {CLI_CALL_TIMEOUT_SECONDS}s") + raise LLMTimeoutError(f"Codex CLI timed out after {CLI_CALL_TIMEOUT_SECONDS}s") def chat_json( self, From d4dafe086d82dc0f2718f7651551a6b6161f8f44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Cartagena?= Date: Sun, 19 Jul 2026 21:14:08 +0200 Subject: [PATCH 7/7] fix(review): resilient env parsing, chunked interview batches, index dedup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _get_int_env helper: malformed MIROFISH_INTERVIEW_MAX_AGENTS values degrade to the default with a warning instead of crashing at import from the Config class body; floor of 1 guards zero/negative panels. - Interview batches now run in chunks of 5 with a per-chunk timeout (ceiling x chunk size): the previous single all-or-nothing IPC transaction scaled to hours with large panels and lost every completed interview if one hung. A failed chunk now loses only itself; results accumulate across chunks. - LLM-selected interview indices are deduped (order-preserving), coerced from strings and range-checked before use. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- app/config.py | 22 +++++++++++++- app/services/graph_tools.py | 57 ++++++++++++++++++++++++++----------- 2 files changed, 61 insertions(+), 18 deletions(-) diff --git a/app/config.py b/app/config.py index cc1a6a7..fbfbf58 100644 --- a/app/config.py +++ b/app/config.py @@ -25,6 +25,26 @@ def _get_bool_env(name: str, default: bool = False) -> bool: return value.strip().lower() in {"1", "true", "yes", "on"} +def _get_int_env(name: str, default: int, min_value: int = 1) -> int: + """Parse an int env var, falling back (with a warning) on bad values. + + Raising at import time from the Config class body produces an opaque + crash before validate() can run, so malformed values degrade to the + default instead. + """ + raw = os.environ.get(name) + if raw is None or not raw.strip(): + return default + try: + value = int(raw.strip()) + except ValueError: + import logging + logging.getLogger("mirofish.config").warning( + "Ignoring invalid %s=%r; using default %s", name, raw, default) + return default + return max(min_value, value) + + def _resolve_path(default_path: str, env_name: str) -> str: raw_value = os.environ.get(env_name, default_path) return os.path.abspath(raw_value) @@ -46,7 +66,7 @@ class Config: # vocal / relevant / silent agents). EXTRA_QUESTIONS (';'-separated) are # appended to the generated questionnaire — useful for closed questions # whose answers can be coded into distributions. - INTERVIEW_MAX_AGENTS = int(os.environ.get("MIROFISH_INTERVIEW_MAX_AGENTS", "5")) + INTERVIEW_MAX_AGENTS = _get_int_env("MIROFISH_INTERVIEW_MAX_AGENTS", 5) INTERVIEW_EXTRA_QUESTIONS = os.environ.get("MIROFISH_INTERVIEW_EXTRA_QUESTIONS", "").strip() DATA_DIR = _resolve_path(os.path.join(os.path.dirname(__file__), "../data/graphs"), "DATA_DIR") diff --git a/app/services/graph_tools.py b/app/services/graph_tools.py index 4e7ad20..6c208ac 100644 --- a/app/services/graph_tools.py +++ b/app/services/graph_tools.py @@ -362,24 +362,40 @@ def interview_agents( for idx in selected_indices ] - # One full CLI-call ceiling per interviewed agent: a fixed 180s budget - # starved whole batches to 0/N with subprocess-based providers. - batch_timeout = float(CLI_CALL_TIMEOUT_SECONDS * max(1, len(interviews_request))) - - api_result = SimulationRunner.interview_agents_batch( - simulation_id=simulation_id, - interviews=interviews_request, - platform=None, - timeout=batch_timeout, - ) - - if not api_result.get("success", False): - result.summary = f"Interview API call failed: {api_result.get('error', 'Unknown error')}" + # Chunked batches, one full CLI-call ceiling per interviewed agent + # (a fixed 180s budget starved whole batches to 0/N). Chunking + # keeps each IPC transaction small so a single hung interview + # loses only its chunk instead of the entire panel, and bounds + # the worst-case wall clock per transaction. + CHUNK_SIZE = 5 + results_dict: Dict[str, Any] = {} + any_chunk_ok = False + for start in range(0, len(interviews_request), CHUNK_SIZE): + chunk = interviews_request[start:start + CHUNK_SIZE] + try: + api_result = SimulationRunner.interview_agents_batch( + simulation_id=simulation_id, + interviews=chunk, + platform=None, + timeout=float(CLI_CALL_TIMEOUT_SECONDS * len(chunk)), + ) + except Exception as chunk_exc: + logger.warning(f"Interview chunk {start // CHUNK_SIZE + 1} failed (non-fatal): {chunk_exc}") + continue + if api_result.get("success", False): + any_chunk_ok = True + api_data = api_result.get("result", {}) + if isinstance(api_data, dict): + results_dict.update(api_data.get("results", {}) or {}) + else: + logger.warning( + f"Interview chunk {start // CHUNK_SIZE + 1} API failure: " + f"{api_result.get('error', 'Unknown error')}") + + if not any_chunk_ok: + result.summary = "Interview API call failed for every chunk" return result - api_data = api_result.get("result", {}) - results_dict = api_data.get("results", {}) if isinstance(api_data, dict) else {} - for i, agent_idx in enumerate(selected_indices): agent = selected_agents[i] agent_name = agent.get("realname", agent.get("username", f"Agent_{agent_idx}")) @@ -556,7 +572,14 @@ def _select_agents_for_interview(self, profiles, interview_requirement, simulati ], temperature=0.3, ) - indices = response.get("selected_indices", [])[:max_agents] + # Dedupe preserving order and coerce/validate: the LLM sometimes + # returns duplicates or stringified indices + raw_indices = response.get("selected_indices", []) + indices = list(dict.fromkeys( + int(i) for i in raw_indices + if isinstance(i, (int, float, str)) and str(i).strip().lstrip("-").isdigit() + and 0 <= int(i) < len(profiles) + ))[:max_agents] reasoning = response.get("reasoning", "Selected based on relevance") agents = [profiles[i] for i in indices if 0 <= i < len(profiles)] valid_indices = [i for i in indices if 0 <= i < len(profiles)]