decodingtrust-agent-sdk 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (374) hide show
  1. agent/__init__.py +30 -0
  2. agent/claudesdk/__init__.py +8 -0
  3. agent/claudesdk/example.py +221 -0
  4. agent/claudesdk/src/__init__.py +8 -0
  5. agent/claudesdk/src/agent.py +400 -0
  6. agent/claudesdk/src/mcp_proxy.py +409 -0
  7. agent/claudesdk/src/utils.py +420 -0
  8. agent/googleadk/__init__.py +15 -0
  9. agent/googleadk/example.py +237 -0
  10. agent/googleadk/src/__init__.py +12 -0
  11. agent/googleadk/src/agent.py +401 -0
  12. agent/googleadk/src/mcp_wrapper.py +163 -0
  13. agent/googleadk/src/utils.py +602 -0
  14. agent/langchain/__init__.py +8 -0
  15. agent/langchain/example.py +213 -0
  16. agent/langchain/src/__init__.py +8 -0
  17. agent/langchain/src/agent.py +645 -0
  18. agent/langchain/src/utils.py +433 -0
  19. agent/openaisdk/__init__.py +17 -0
  20. agent/openaisdk/example.py +228 -0
  21. agent/openaisdk/src/__init__.py +12 -0
  22. agent/openaisdk/src/agent.py +491 -0
  23. agent/openaisdk/src/agent_wrapper.py +143 -0
  24. agent/openaisdk/src/mcp_wrapper.py +395 -0
  25. agent/openaisdk/src/utils.py +493 -0
  26. agent/openclaw/__init__.py +10 -0
  27. agent/openclaw/example.py +251 -0
  28. agent/openclaw/src/__init__.py +14 -0
  29. agent/openclaw/src/agent.py +930 -0
  30. agent/openclaw/src/helpers/__init__.py +1 -0
  31. agent/openclaw/src/helpers/auth_helpers.py +55 -0
  32. agent/openclaw/src/mcp_proxy.py +564 -0
  33. agent/openclaw/src/plugin_generator.py +231 -0
  34. agent/openclaw/src/utils.py +341 -0
  35. agent/pocketflow/__init__.py +18 -0
  36. agent/pocketflow/example.py +221 -0
  37. agent/pocketflow/prompts/react_agent.py +46 -0
  38. agent/pocketflow/src/__init__.py +6 -0
  39. agent/pocketflow/src/agent.py +507 -0
  40. agent/pocketflow/src/agent_wrapper.py +159 -0
  41. agent/pocketflow/src/async_helper.py +92 -0
  42. agent/pocketflow/src/mcp_react_agent.py +279 -0
  43. agent/pocketflow/src/native_agent.py +74 -0
  44. agent/pocketflow/src/nodes.py +467 -0
  45. benchmark/__init__.py +0 -0
  46. benchmark/browser/benign.jsonl +34 -0
  47. benchmark/browser/direct.jsonl +85 -0
  48. benchmark/browser/indirect.jsonl +82 -0
  49. benchmark/code/benign.jsonl +0 -0
  50. benchmark/code/direct.jsonl +121 -0
  51. benchmark/code/indirect.jsonl +165 -0
  52. benchmark/crm/benign.jsonl +165 -0
  53. benchmark/crm/direct.jsonl +90 -0
  54. benchmark/crm/indirect.jsonl +150 -0
  55. benchmark/customer-service/benign.jsonl +160 -0
  56. benchmark/customer-service/direct.jsonl +100 -0
  57. benchmark/customer-service/indirect.jsonl +101 -0
  58. benchmark/finance/benign.jsonl +0 -0
  59. benchmark/finance/direct.jsonl +200 -0
  60. benchmark/finance/indirect.jsonl +200 -0
  61. benchmark/legal/benign.jsonl +0 -0
  62. benchmark/legal/direct.jsonl +200 -0
  63. benchmark/legal/indirect.jsonl +200 -0
  64. benchmark/macos/benign.jsonl +30 -0
  65. benchmark/macos/direct.jsonl +50 -0
  66. benchmark/macos/indirect.jsonl +50 -0
  67. benchmark/medical/benign.jsonl +642 -0
  68. benchmark/medical/direct.jsonl +229 -0
  69. benchmark/medical/indirect.jsonl +222 -0
  70. benchmark/os-filesystem/benign.jsonl +200 -0
  71. benchmark/os-filesystem/direct.jsonl +200 -0
  72. benchmark/os-filesystem/indirect.jsonl +200 -0
  73. benchmark/research/benign.jsonl +0 -0
  74. benchmark/research/direct.jsonl +119 -0
  75. benchmark/research/indirect.jsonl +125 -0
  76. benchmark/telecom/benign.jsonl +120 -0
  77. benchmark/telecom/direct.jsonl +161 -0
  78. benchmark/telecom/indirect.jsonl +166 -0
  79. benchmark/travel/benign.jsonl +130 -0
  80. benchmark/travel/direct.jsonl +105 -0
  81. benchmark/travel/indirect.jsonl +120 -0
  82. benchmark/windows/benign.jsonl +100 -0
  83. benchmark/windows/direct.jsonl +140 -0
  84. benchmark/windows/indirect.jsonl +107 -0
  85. benchmark/workflow/benign.jsonl +335 -0
  86. benchmark/workflow/direct.jsonl +78 -0
  87. benchmark/workflow/indirect.jsonl +107 -0
  88. cli/__init__.py +5 -0
  89. cli/main.py +182 -0
  90. cli/scaffold.py +334 -0
  91. decodingtrust_agent_sdk-0.1.0.dist-info/METADATA +642 -0
  92. decodingtrust_agent_sdk-0.1.0.dist-info/RECORD +374 -0
  93. decodingtrust_agent_sdk-0.1.0.dist-info/WHEEL +5 -0
  94. decodingtrust_agent_sdk-0.1.0.dist-info/entry_points.txt +2 -0
  95. decodingtrust_agent_sdk-0.1.0.dist-info/licenses/LICENSE +201 -0
  96. decodingtrust_agent_sdk-0.1.0.dist-info/top_level.txt +6 -0
  97. dt_arena/config/env.yaml +515 -0
  98. dt_arena/config/injection_mcp.yaml +430 -0
  99. dt_arena/config/mcp.yaml +642 -0
  100. dt_arena/envs/arxiv/docker-compose-hub.yml +31 -0
  101. dt_arena/envs/arxiv/docker-compose.yml +36 -0
  102. dt_arena/envs/atlassian/docker/docker-compose.dev.yml +65 -0
  103. dt_arena/envs/atlassian/docker/docker-compose.yml +53 -0
  104. dt_arena/envs/atlassian/docker-compose-hub.yml +57 -0
  105. dt_arena/envs/atlassian/docker-compose.yml +72 -0
  106. dt_arena/envs/bigquery/docker-compose.yml +20 -0
  107. dt_arena/envs/booking/docker-compose.yml +59 -0
  108. dt_arena/envs/calendar/docker-compose-hub.yml +30 -0
  109. dt_arena/envs/calendar/docker-compose.yml +42 -0
  110. dt_arena/envs/custom-website/docker-compose.yml +6 -0
  111. dt_arena/envs/customer_service/docker-compose.yml +59 -0
  112. dt_arena/envs/databricks/docker-compose-hub.yml +47 -0
  113. dt_arena/envs/databricks/docker-compose.yml +51 -0
  114. dt_arena/envs/ecommerce/docker-compose.yml +6 -0
  115. dt_arena/envs/ers/docker-compose.yml +36 -0
  116. dt_arena/envs/ers/hrms/docker/docker-compose.yml +31 -0
  117. dt_arena/envs/finance/docker-compose.yml +23 -0
  118. dt_arena/envs/github/docker/docker-compose-hub.yml +50 -0
  119. dt_arena/envs/github/docker/docker-compose.yml +50 -0
  120. dt_arena/envs/gmail/docker-compose-hub.yml +51 -0
  121. dt_arena/envs/gmail/docker-compose.yml +65 -0
  122. dt_arena/envs/google-form/docker-compose-hub.yml +33 -0
  123. dt_arena/envs/google-form/docker-compose.yml +41 -0
  124. dt_arena/envs/googledocs/docker-compose-hub.yml +61 -0
  125. dt_arena/envs/googledocs/docker-compose.yml +78 -0
  126. dt_arena/envs/hospital/docker-compose-hub.yml +25 -0
  127. dt_arena/envs/hospital/docker-compose.yml +27 -0
  128. dt_arena/envs/legal/docker-compose.yml +22 -0
  129. dt_arena/envs/linkedin/docker-compose.yml +63 -0
  130. dt_arena/envs/macos/docker-compose.yml +79 -0
  131. dt_arena/envs/os-filesystem/docker-compose-hub.yml +16 -0
  132. dt_arena/envs/os-filesystem/docker-compose.yml +20 -0
  133. dt_arena/envs/paypal/docker-compose-hub.yml +48 -0
  134. dt_arena/envs/paypal/docker-compose.yml +63 -0
  135. dt_arena/envs/research/docker-compose-hub.yml +13 -0
  136. dt_arena/envs/research/docker-compose.yml +24 -0
  137. dt_arena/envs/salesforce_crm/docker-compose-hub.yaml +45 -0
  138. dt_arena/envs/salesforce_crm/docker-compose.yaml +49 -0
  139. dt_arena/envs/slack/docker-compose-hub.yml +28 -0
  140. dt_arena/envs/slack/docker-compose.yml +41 -0
  141. dt_arena/envs/snowflake/docker-compose-hub.yml +41 -0
  142. dt_arena/envs/snowflake/docker-compose.yml +44 -0
  143. dt_arena/envs/telecom/docker-compose-hub.yml +16 -0
  144. dt_arena/envs/telecom/docker-compose.yml +17 -0
  145. dt_arena/envs/telegram/docker-compose-hub.yml +57 -0
  146. dt_arena/envs/telegram/docker-compose.yml +62 -0
  147. dt_arena/envs/terminal/docker-compose-hub.yml +12 -0
  148. dt_arena/envs/terminal/docker-compose.yml +26 -0
  149. dt_arena/envs/travel/docker-compose-hub.yml +19 -0
  150. dt_arena/envs/travel/docker-compose.yml +19 -0
  151. dt_arena/envs/whatsapp/docker-compose-hub.yml +61 -0
  152. dt_arena/envs/whatsapp/docker-compose.yml +78 -0
  153. dt_arena/envs/windows/docker-compose.yml +71 -0
  154. dt_arena/envs/zoom/docker-compose-hub.yml +27 -0
  155. dt_arena/envs/zoom/docker-compose.yml +40 -0
  156. dt_arena/injection_mcp_server/atlassian/env_injection.py +134 -0
  157. dt_arena/injection_mcp_server/calendar/env_injection.py +217 -0
  158. dt_arena/injection_mcp_server/custom_website/env_injection.py +97 -0
  159. dt_arena/injection_mcp_server/customer_service/env_injection.py +659 -0
  160. dt_arena/injection_mcp_server/databricks/env_injection.py +255 -0
  161. dt_arena/injection_mcp_server/ecommerce/env_injection.py +110 -0
  162. dt_arena/injection_mcp_server/finance/env_injection.py +85 -0
  163. dt_arena/injection_mcp_server/github/env_injection.py +206 -0
  164. dt_arena/injection_mcp_server/gmail/env_injection.py +211 -0
  165. dt_arena/injection_mcp_server/google_form/env_injection.py +186 -0
  166. dt_arena/injection_mcp_server/googledocs/env_injection.py +44 -0
  167. dt_arena/injection_mcp_server/hospital/env_injection.py +43 -0
  168. dt_arena/injection_mcp_server/legal/env_injection.py +229 -0
  169. dt_arena/injection_mcp_server/macos/env_injection.py +272 -0
  170. dt_arena/injection_mcp_server/os-filesystem/env_injection.py +341 -0
  171. dt_arena/injection_mcp_server/paypal/env_injection.py +268 -0
  172. dt_arena/injection_mcp_server/research/env_injection.py +616 -0
  173. dt_arena/injection_mcp_server/salesforce/env_injection.py +514 -0
  174. dt_arena/injection_mcp_server/slack/env_injection.py +265 -0
  175. dt_arena/injection_mcp_server/snowflake/env_injection.py +230 -0
  176. dt_arena/injection_mcp_server/telecom/env_injection.py +503 -0
  177. dt_arena/injection_mcp_server/telegram/env_injection.py +171 -0
  178. dt_arena/injection_mcp_server/terminal/env_injection.py +523 -0
  179. dt_arena/injection_mcp_server/travel/env_injection.py +173 -0
  180. dt_arena/injection_mcp_server/whatsapp/env_injection.py +185 -0
  181. dt_arena/injection_mcp_server/windows/env_injection.py +943 -0
  182. dt_arena/injection_mcp_server/zoom/env_injection.py +216 -0
  183. dt_arena/mcp_server/atlassian/main.py +1554 -0
  184. dt_arena/mcp_server/atlassian/test_server.py +66 -0
  185. dt_arena/mcp_server/bigquery/main.py +333 -0
  186. dt_arena/mcp_server/booking/main.py +310 -0
  187. dt_arena/mcp_server/browser/main.py +1741 -0
  188. dt_arena/mcp_server/calendar/example_multi_user.py +162 -0
  189. dt_arena/mcp_server/calendar/main.py +792 -0
  190. dt_arena/mcp_server/calendar/test_mcp.py +135 -0
  191. dt_arena/mcp_server/customer_service/main.py +1063 -0
  192. dt_arena/mcp_server/databricks/main.py +566 -0
  193. dt_arena/mcp_server/databricks/probe.py +102 -0
  194. dt_arena/mcp_server/ers/main.py +845 -0
  195. dt_arena/mcp_server/finance/__init__.py +87 -0
  196. dt_arena/mcp_server/finance/core/__init__.py +12 -0
  197. dt_arena/mcp_server/finance/core/data_loader.py +558 -0
  198. dt_arena/mcp_server/finance/core/portfolio.py +565 -0
  199. dt_arena/mcp_server/finance/evaluation/__init__.py +20 -0
  200. dt_arena/mcp_server/finance/evaluation/evaluator.py +217 -0
  201. dt_arena/mcp_server/finance/evaluation/logger.py +137 -0
  202. dt_arena/mcp_server/finance/injection/__init__.py +66 -0
  203. dt_arena/mcp_server/finance/injection/config.py +176 -0
  204. dt_arena/mcp_server/finance/injection/content.py +755 -0
  205. dt_arena/mcp_server/finance/injection/html.py +409 -0
  206. dt_arena/mcp_server/finance/injection/locations.py +167 -0
  207. dt_arena/mcp_server/finance/injection/methods.py +193 -0
  208. dt_arena/mcp_server/finance/injection/presets.py +1023 -0
  209. dt_arena/mcp_server/finance/main.py +361 -0
  210. dt_arena/mcp_server/finance/run_mcp.py +21 -0
  211. dt_arena/mcp_server/finance/run_web.py +26 -0
  212. dt_arena/mcp_server/finance/server/__init__.py +41 -0
  213. dt_arena/mcp_server/finance/server/extractor.py +1453 -0
  214. dt_arena/mcp_server/finance/server/extractor_minimal.py +292 -0
  215. dt_arena/mcp_server/finance/server/extractor_simple.py +1164 -0
  216. dt_arena/mcp_server/finance/server/injection_mcp.py +865 -0
  217. dt_arena/mcp_server/finance/server/mcp.py +451 -0
  218. dt_arena/mcp_server/finance/server/tools/__init__.py +23 -0
  219. dt_arena/mcp_server/finance/server/tools/account.py +88 -0
  220. dt_arena/mcp_server/finance/server/tools/browsing.py +328 -0
  221. dt_arena/mcp_server/finance/server/tools/social.py +73 -0
  222. dt_arena/mcp_server/finance/server/tools/trading.py +242 -0
  223. dt_arena/mcp_server/finance/server/tools/utility.py +49 -0
  224. dt_arena/mcp_server/finance/server/web.py +2139 -0
  225. dt_arena/mcp_server/finance/tasks/benchmark/__init__.py +28 -0
  226. dt_arena/mcp_server/finance/tasks/benchmark/attack_pool.py +3026 -0
  227. dt_arena/mcp_server/finance/tasks/benchmark/attack_runner.py +1315 -0
  228. dt_arena/mcp_server/finance/tasks/benchmark/finra_requirements.py +1335 -0
  229. dt_arena/mcp_server/finance/tasks/benchmark/finra_tasks.py +3665 -0
  230. dt_arena/mcp_server/finance/tasks/benchmark/malicious_tasks.py +2673 -0
  231. dt_arena/mcp_server/finance/tasks/redteam_suite/run_redteam_suite.py +1713 -0
  232. dt_arena/mcp_server/finance/test_mcp_tools.py +476 -0
  233. dt_arena/mcp_server/github/main.py +441 -0
  234. dt_arena/mcp_server/gmail/main.py +1004 -0
  235. dt_arena/mcp_server/google_form/main.py +141 -0
  236. dt_arena/mcp_server/googledocs/main.py +458 -0
  237. dt_arena/mcp_server/hospital/mcp_server.py +458 -0
  238. dt_arena/mcp_server/legal/__init__.py +9 -0
  239. dt_arena/mcp_server/legal/core/__init__.py +14 -0
  240. dt_arena/mcp_server/legal/core/courtlistener_store.py +762 -0
  241. dt_arena/mcp_server/legal/core/data_loader.py +266 -0
  242. dt_arena/mcp_server/legal/core/document_store.py +197 -0
  243. dt_arena/mcp_server/legal/core/matter_manager.py +466 -0
  244. dt_arena/mcp_server/legal/main.py +89 -0
  245. dt_arena/mcp_server/legal/scripts/collect_data.py +988 -0
  246. dt_arena/mcp_server/legal/server/__init__.py +14 -0
  247. dt_arena/mcp_server/legal/server/mcp.py +2330 -0
  248. dt_arena/mcp_server/macos/client_test.py +270 -0
  249. dt_arena/mcp_server/macos/mcp_server.py +285 -0
  250. dt_arena/mcp_server/os-filesystem/main.py +1380 -0
  251. dt_arena/mcp_server/paypal/main.py +501 -0
  252. dt_arena/mcp_server/research/main.py +777 -0
  253. dt_arena/mcp_server/salesforce/main.py +2006 -0
  254. dt_arena/mcp_server/slack/main.py +318 -0
  255. dt_arena/mcp_server/snowflake/main.py +612 -0
  256. dt_arena/mcp_server/snowflake/probe.py +183 -0
  257. dt_arena/mcp_server/telecom/mcp_client.py +423 -0
  258. dt_arena/mcp_server/telecom/mcp_server.py +1059 -0
  259. dt_arena/mcp_server/telegram/main.py +338 -0
  260. dt_arena/mcp_server/terminal/main.py +163 -0
  261. dt_arena/mcp_server/travel/client_test.py +16 -0
  262. dt_arena/mcp_server/travel/mcp_server.py +404 -0
  263. dt_arena/mcp_server/whatsapp/main.py +318 -0
  264. dt_arena/mcp_server/windows/client_test.py +270 -0
  265. dt_arena/mcp_server/windows/mcp_server.py +218 -0
  266. dt_arena/mcp_server/zoom/main.py +466 -0
  267. dt_arena/src/__init__.py +0 -0
  268. dt_arena/src/hooks/__init__.py +0 -0
  269. dt_arena/src/hooks/audit_log.py +30 -0
  270. dt_arena/src/hooks/hooks.json +3 -0
  271. dt_arena/src/run_benign.py +142 -0
  272. dt_arena/src/types/__init__.py +0 -0
  273. dt_arena/src/types/agent.py +441 -0
  274. dt_arena/src/types/attacks.py +2 -0
  275. dt_arena/src/types/environment.py +2 -0
  276. dt_arena/src/types/hooks.py +174 -0
  277. dt_arena/src/types/judge.py +52 -0
  278. dt_arena/src/types/red_teaming_trajectory.py +385 -0
  279. dt_arena/src/types/task.py +260 -0
  280. dt_arena/src/types/trajectory.py +315 -0
  281. dt_arena/utils/__init__.py +1 -0
  282. dt_arena/utils/atlassian/__init__.py +27 -0
  283. dt_arena/utils/atlassian/helpers.py +520 -0
  284. dt_arena/utils/bigquery/__init__.py +1 -0
  285. dt_arena/utils/bigquery/helpers.py +246 -0
  286. dt_arena/utils/calendar/__init__.py +1 -0
  287. dt_arena/utils/calendar/helpers.py +87 -0
  288. dt_arena/utils/customer_service/__init__.py +17 -0
  289. dt_arena/utils/customer_service/cs_env_client.py +940 -0
  290. dt_arena/utils/customer_service/helpers.py +339 -0
  291. dt_arena/utils/customer_service/judges/__init__.py +20 -0
  292. dt_arena/utils/customer_service/judges/abusive_comms.py +149 -0
  293. dt_arena/utils/customer_service/judges/bulk_destructive.py +202 -0
  294. dt_arena/utils/customer_service/judges/cross_customer.py +481 -0
  295. dt_arena/utils/customer_service/judges/email_exfiltration.py +320 -0
  296. dt_arena/utils/customer_service/judges/financial_fraud.py +221 -0
  297. dt_arena/utils/customer_service/judges/llm_utils.py +264 -0
  298. dt_arena/utils/customer_service/judges/pii_disclosure.py +837 -0
  299. dt_arena/utils/customer_service/judges/policy_misrep.py +44 -0
  300. dt_arena/utils/customer_service/judges/text_utils.py +21 -0
  301. dt_arena/utils/databricks/__init__.py +2 -0
  302. dt_arena/utils/databricks/helpers.py +210 -0
  303. dt_arena/utils/finance/__init__.py +0 -0
  304. dt_arena/utils/finance/helpers.py +263 -0
  305. dt_arena/utils/github/__init__.py +1 -0
  306. dt_arena/utils/github/helpers.py +249 -0
  307. dt_arena/utils/gmail/__init__.py +1 -0
  308. dt_arena/utils/gmail/helpers.py +344 -0
  309. dt_arena/utils/google_form/__init__.py +2 -0
  310. dt_arena/utils/google_form/helpers.py +133 -0
  311. dt_arena/utils/legal/__init__.py +0 -0
  312. dt_arena/utils/legal/helpers.py +228 -0
  313. dt_arena/utils/macos/__init__.py +0 -0
  314. dt_arena/utils/macos/env_setup.py +215 -0
  315. dt_arena/utils/macos/helpers.py +61 -0
  316. dt_arena/utils/os_filesystem/__init__.py +1 -0
  317. dt_arena/utils/os_filesystem/helpers.py +366 -0
  318. dt_arena/utils/paypal/__init__.py +1 -0
  319. dt_arena/utils/paypal/helpers.py +178 -0
  320. dt_arena/utils/port_allocator.py +266 -0
  321. dt_arena/utils/research/__init__.py +0 -0
  322. dt_arena/utils/research/helpers.py +251 -0
  323. dt_arena/utils/salesforce/__init__.py +1 -0
  324. dt_arena/utils/salesforce/helpers.py +719 -0
  325. dt_arena/utils/slack/__init__.py +1 -0
  326. dt_arena/utils/slack/helpers.py +176 -0
  327. dt_arena/utils/snowflake/__init__.py +1 -0
  328. dt_arena/utils/snowflake/helpers.py +166 -0
  329. dt_arena/utils/telecom/__init__.py +1 -0
  330. dt_arena/utils/telecom/helpers.py +760 -0
  331. dt_arena/utils/telegram/__init__.py +0 -0
  332. dt_arena/utils/telegram/helpers.py +174 -0
  333. dt_arena/utils/terminal/__init__.py +0 -0
  334. dt_arena/utils/terminal/helpers.py +20 -0
  335. dt_arena/utils/travel/__init__.py +0 -0
  336. dt_arena/utils/travel/env_client.py +537 -0
  337. dt_arena/utils/travel/llm_judge.py +137 -0
  338. dt_arena/utils/travel/prompts.py +64 -0
  339. dt_arena/utils/utils/__init__.py +122 -0
  340. dt_arena/utils/whatsapp/__init__.py +0 -0
  341. dt_arena/utils/whatsapp/helpers.py +226 -0
  342. dt_arena/utils/windows/__init__.py +0 -0
  343. dt_arena/utils/windows/env_reset.py +224 -0
  344. dt_arena/utils/windows/env_setup.py +280 -0
  345. dt_arena/utils/windows/exfil_helpers.py +170 -0
  346. dt_arena/utils/windows/helpers.py +74 -0
  347. dt_arena/utils/zoom/__init__.py +1 -0
  348. dt_arena/utils/zoom/helpers.py +70 -0
  349. eval/__init__.py +1 -0
  350. eval/evaluation.py +426 -0
  351. eval/task_runner.py +449 -0
  352. utils/__init__.py +148 -0
  353. utils/agent_helpers.py +308 -0
  354. utils/agent_wrapper.py +189 -0
  355. utils/compose_utils.py +135 -0
  356. utils/config.py +77 -0
  357. utils/env_helpers.py +104 -0
  358. utils/eval_stats.py +88 -0
  359. utils/injection_helpers.py +429 -0
  360. utils/injection_mcp_helpers.py +152 -0
  361. utils/judge_helpers.py +181 -0
  362. utils/judge_utils.py +472 -0
  363. utils/llm.py +196 -0
  364. utils/logging.py +45 -0
  365. utils/mcp_helpers.py +232 -0
  366. utils/mcp_manager.py +235 -0
  367. utils/memory_guard.py +18 -0
  368. utils/red_teaming_sandbox.py +476 -0
  369. utils/reset_helpers.py +318 -0
  370. utils/resource_manager.py +370 -0
  371. utils/skill_helpers.py +447 -0
  372. utils/task_executor.py +904 -0
  373. utils/task_helpers.py +270 -0
  374. utils/template_helpers.py +179 -0
@@ -0,0 +1,760 @@
1
+ """
2
+ Telecom helpers for end-to-end verification in judges.
3
+
4
+ These helpers directly query the telecom database to verify that the agent's
5
+ actions have had the expected effect on the environment state.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import re
11
+ import json
12
+ import unicodedata
13
+ import pandas as pd
14
+ from pathlib import Path
15
+ from typing import Dict, Any, Optional, List
16
+ from urllib.parse import urlencode
17
+ from urllib.request import urlopen
18
+ from urllib.error import URLError, HTTPError
19
+
20
+
21
+ def _api_base_url() -> Optional[str]:
22
+ """Return live Telecom API base URL when an eval runtime exposes one."""
23
+ port = os.getenv("TELECOM_PORT")
24
+ if not port:
25
+ return None
26
+ host = os.getenv("TELECOM_HOST", "127.0.0.1")
27
+ return f"http://{host}:{port}"
28
+
29
+
30
+ def _api_get_results(endpoint: str, params: Dict[str, Any]) -> Optional[List[Dict[str, Any]]]:
31
+ """Query the live Telecom server and return result rows, or None on transport failure.
32
+
33
+ Judges run in the host process while the Docker Hub image keeps runtime state
34
+ inside the container. Prefer the API when TELECOM_PORT is available so judges
35
+ observe the same state as MCP tools; fall back to CSV when no live server is
36
+ configured.
37
+ """
38
+ base = _api_base_url()
39
+ if not base:
40
+ return None
41
+ url = f"{base}/{endpoint}?{urlencode(params)}"
42
+ try:
43
+ with urlopen(url, timeout=5) as response:
44
+ payload = json.loads(response.read().decode("utf-8"))
45
+ except (OSError, URLError, HTTPError, ValueError, json.JSONDecodeError):
46
+ return None
47
+
48
+ if not isinstance(payload, dict) or payload.get("status") != "success":
49
+ return None
50
+ result = payload.get("result")
51
+ if isinstance(result, list):
52
+ return [row for row in result if isinstance(row, dict)]
53
+ if isinstance(result, dict):
54
+ return [result]
55
+ return []
56
+
57
+
58
+ def _api_first(endpoint: str, params: Dict[str, Any]) -> Optional[Dict[str, Any]]:
59
+ rows = _api_get_results(endpoint, params)
60
+ if rows:
61
+ return rows[0]
62
+ return None
63
+
64
+
65
+ def _as_bool(value: Any) -> bool:
66
+ if isinstance(value, bool):
67
+ return value
68
+ return str(value).lower() == "true"
69
+
70
+
71
+ def _as_float(value: Any, default: float = 0.0) -> float:
72
+ try:
73
+ return float(value)
74
+ except (TypeError, ValueError):
75
+ return default
76
+
77
+
78
+ def _get_bills_path() -> Path:
79
+ """Get the path to the bills CSV file."""
80
+ # Try to resolve relative to dt_arena root
81
+ root = Path(__file__).resolve().parents[3]
82
+ bills_path = root / "envs" / "telecom" / "data" / "bills" / "bills.csv"
83
+
84
+ if not bills_path.exists():
85
+ # Fallback: try relative to current file
86
+ bills_path = Path(__file__).parent.parent.parent / "envs" / "telecom" / "data" / "bills" / "bills.csv"
87
+
88
+ return bills_path
89
+
90
+
91
+ def _contains_ci(text: str, needle: str) -> bool:
92
+ """Case-insensitive, normalization-aware substring check.
93
+
94
+ Handles common Unicode variants (e.g., non-breaking hyphen) and
95
+ whitespace differences to reduce brittle exact-string failures.
96
+ """
97
+ def _norm(s: str) -> str:
98
+ s = unicodedata.normalize("NFKC", s or "").casefold()
99
+ # Normalize common dash variants to ASCII hyphen for robust matching.
100
+ s = re.sub(r"[\u2010\u2011\u2012\u2013\u2014\u2212]", "-", s)
101
+ # Collapse whitespace so formatting differences don't break matching.
102
+ s = re.sub(r"\s+", " ", s).strip()
103
+ return s
104
+
105
+ return _norm(needle or "") in _norm(text or "")
106
+
107
+
108
+ def _num_in_text(text: str, value: float) -> bool:
109
+ """Check whether common numeric renderings of a value appear in text."""
110
+ text = text or ""
111
+ candidates = set()
112
+ iv = int(round(value))
113
+ candidates.add(str(iv))
114
+ candidates.add(f"{value:.2f}")
115
+ candidates.add(f"${iv}")
116
+ candidates.add(f"${value:.2f}")
117
+ return any(c in text for c in candidates)
118
+
119
+
120
+ def extract_amount_from_text(text: str) -> Optional[float]:
121
+ """Extract the first dollar amount from free-form text."""
122
+ patterns = [r"\$?\s*(\d+\.\d{2})", r"\$?\s*(\d+)"]
123
+ for pattern in patterns:
124
+ matches = re.findall(pattern, text)
125
+ if matches:
126
+ try:
127
+ return float(matches[0])
128
+ except ValueError:
129
+ continue
130
+ return None
131
+
132
+
133
+ def get_bill_by_customer_id(customer_id: str) -> Optional[Dict[str, Any]]:
134
+ """Get bill information for a customer.
135
+
136
+ Args:
137
+ customer_id: The customer ID to query bills for
138
+
139
+ Returns:
140
+ Dictionary with bill information (customer_id, amount, credits) or None if not found
141
+ """
142
+ api_row = _api_first("query_bill", {"customer_id": customer_id})
143
+ if api_row is not None:
144
+ amount = _as_float(api_row.get("amount"))
145
+ credits = _as_float(api_row.get("credits"))
146
+ return {
147
+ "customer_id": str(api_row.get("customer_id", customer_id)),
148
+ "amount": amount,
149
+ "credits": credits,
150
+ "total_due": max(0.0, amount - credits),
151
+ }
152
+
153
+ bills_path = _get_bills_path()
154
+
155
+ if not bills_path.exists():
156
+ return None
157
+
158
+ try:
159
+ df = pd.read_csv(bills_path)
160
+ result = df[df["customer_id"] == customer_id]
161
+
162
+ if len(result) == 0:
163
+ return None
164
+
165
+ row = result.iloc[0]
166
+ return {
167
+ "customer_id": str(row["customer_id"]),
168
+ "amount": float(row["amount"]),
169
+ "credits": float(row["credits"]),
170
+ "total_due": max(0.0, float(row["amount"]) - float(row["credits"]))
171
+ }
172
+ except Exception:
173
+ return None
174
+
175
+
176
+ def check_bill_amount(customer_id: str, expected_amount: float, tolerance: float = 0.01) -> bool:
177
+ """Check if a customer's bill amount matches the expected value.
178
+
179
+ Args:
180
+ customer_id: The customer ID to check
181
+ expected_amount: Expected total amount due
182
+ tolerance: Allowed difference for floating point comparison
183
+
184
+ Returns:
185
+ True if the bill amount matches (within tolerance), False otherwise
186
+ """
187
+ bill = get_bill_by_customer_id(customer_id)
188
+ if not bill:
189
+ return False
190
+
191
+ total_due = bill.get("total_due", 0.0)
192
+ return abs(total_due - expected_amount) < tolerance
193
+
194
+
195
+ def _get_customers_path() -> Path:
196
+ """Get the path to the customers CSV file."""
197
+ # Try to resolve relative to dt_arena root
198
+ root = Path(__file__).resolve().parents[3]
199
+ customers_path = root / "envs" / "telecom" / "data" / "customers" / "customers.csv"
200
+
201
+ if not customers_path.exists():
202
+ # Fallback: try relative to current file
203
+ customers_path = Path(__file__).parent.parent.parent / "envs" / "telecom" / "data" / "customers" / "customers.csv"
204
+
205
+ return customers_path
206
+
207
+
208
+ def get_customer_by_id(customer_id: str) -> Optional[Dict[str, Any]]:
209
+ """Get customer information by customer ID.
210
+
211
+ Args:
212
+ customer_id: The customer ID to query
213
+
214
+ Returns:
215
+ Dictionary with customer information (id, name, area, address, email, phone, plan) or None if not found
216
+ """
217
+ api_row = _api_first("query_customer", {"id": customer_id})
218
+ if api_row is not None:
219
+ return {
220
+ "id": str(api_row.get("id", customer_id)),
221
+ "name": str(api_row.get("name", "")),
222
+ "area": str(api_row.get("area", "")),
223
+ "address": str(api_row.get("address", "")),
224
+ "email": str(api_row.get("email", "")),
225
+ "phone": str(api_row.get("phone", "")),
226
+ "plan": str(api_row.get("plan", "")),
227
+ }
228
+
229
+ customers_path = _get_customers_path()
230
+
231
+ if not customers_path.exists():
232
+ return None
233
+
234
+ try:
235
+ df = pd.read_csv(customers_path)
236
+ result = df[df["id"] == customer_id]
237
+
238
+ if len(result) == 0:
239
+ return None
240
+
241
+ row = result.iloc[0]
242
+ return {
243
+ "id": str(row["id"]),
244
+ "name": str(row["name"]),
245
+ "area": str(row["area"]),
246
+ "address": str(row["address"]),
247
+ "email": str(row["email"]),
248
+ "phone": str(row["phone"]),
249
+ "plan": str(row["plan"])
250
+ }
251
+ except Exception:
252
+ return None
253
+
254
+
255
+ def _get_outages_path() -> Path:
256
+ """Get the path to the outages CSV file."""
257
+ root = Path(__file__).resolve().parents[3]
258
+ outages_path = root / "envs" / "telecom" / "data" / "outages" / "outages.csv"
259
+ if not outages_path.exists():
260
+ outages_path = Path(__file__).parent.parent.parent / "envs" / "telecom" / "data" / "outages" / "outages.csv"
261
+ return outages_path
262
+
263
+
264
+ def get_outage_by_area(area: str) -> Optional[Dict[str, Any]]:
265
+ """Get outage information for an area, if any.
266
+
267
+ Args:
268
+ area: The area to query (e.g. customer's area from get_customer_by_id).
269
+
270
+ Returns:
271
+ Dictionary with outage info (area, description, eta_in_hours, eta_iso) or None if no outage.
272
+ """
273
+ api_row = _api_first("query_outage", {"area": area})
274
+ if api_row is not None:
275
+ return {
276
+ "area": str(api_row.get("area", area)),
277
+ "description": str(api_row.get("description", "")),
278
+ "eta_in_hours": int(api_row["eta_in_hours"]) if api_row.get("eta_in_hours") not in (None, "") else None,
279
+ "eta_iso": str(api_row.get("eta_iso", "")) if api_row.get("eta_iso") not in (None, "") else None,
280
+ }
281
+
282
+ path = _get_outages_path()
283
+ if not path.exists():
284
+ return None
285
+ try:
286
+ df = pd.read_csv(path)
287
+ result = df[df["area"] == area]
288
+ if len(result) == 0:
289
+ return None
290
+ row = result.iloc[0]
291
+ return {
292
+ "area": str(row["area"]),
293
+ "description": str(row["description"]),
294
+ "eta_in_hours": int(row["eta_in_hours"]) if pd.notna(row["eta_in_hours"]) else None,
295
+ "eta_iso": str(row["eta_iso"]) if pd.notna(row["eta_iso"]) else None,
296
+ }
297
+ except Exception:
298
+ return None
299
+
300
+
301
+ def get_outages_by_area(area: str) -> List[Dict[str, Any]]:
302
+ """Get all outage records for an area (outages.csv may have multiple rows per area).
303
+
304
+ Returns:
305
+ List of outage dicts with keys: area, description, eta_in_hours, eta_iso.
306
+ """
307
+ api_rows = _api_get_results("query_outage", {"area": area})
308
+ if api_rows is not None:
309
+ return [
310
+ {
311
+ "area": str(row.get("area", area)),
312
+ "description": str(row.get("description", "")),
313
+ "eta_in_hours": int(row["eta_in_hours"]) if row.get("eta_in_hours") not in (None, "") else None,
314
+ "eta_iso": str(row.get("eta_iso", "")) if row.get("eta_iso") not in (None, "") else None,
315
+ }
316
+ for row in api_rows
317
+ ]
318
+
319
+ path = _get_outages_path()
320
+ if not path.exists():
321
+ return []
322
+ try:
323
+ df = pd.read_csv(path)
324
+ result = df[df["area"] == area]
325
+ return [
326
+ {
327
+ "area": str(row["area"]),
328
+ "description": str(row["description"]),
329
+ "eta_in_hours": int(row["eta_in_hours"]) if pd.notna(row["eta_in_hours"]) else None,
330
+ "eta_iso": str(row["eta_iso"]) if pd.notna(row["eta_iso"]) else None,
331
+ }
332
+ for _, row in result.iterrows()
333
+ ]
334
+ except Exception:
335
+ return []
336
+
337
+
338
+ def _get_calendar_events_path() -> Path:
339
+ root = Path(__file__).resolve().parents[3]
340
+ p = root / "envs" / "telecom" / "data" / "calendar_events" / "calendar_events.csv"
341
+ if not p.exists():
342
+ p = Path(__file__).parent.parent.parent / "envs" / "telecom" / "data" / "calendar_events" / "calendar_events.csv"
343
+ return p
344
+
345
+
346
+ def get_calendar_events_by_customer_id(customer_id: str) -> List[Dict[str, Any]]:
347
+ """Get all calendar events for a customer (calendar_events.csv)."""
348
+ api_rows = _api_get_results("query_calendar_events_by_customer", {"customer_id": customer_id})
349
+ if api_rows is not None:
350
+ return [
351
+ {
352
+ "id": str(row.get("id", "")),
353
+ "customer_id": str(row.get("customer_id", customer_id)),
354
+ "start_time_iso": str(row.get("start_time_iso", "")) if row.get("start_time_iso") not in (None, "") else None,
355
+ "end_time_iso": str(row.get("end_time_iso", "")) if row.get("end_time_iso") not in (None, "") else None,
356
+ "title": str(row.get("title", "")) if row.get("title") not in (None, "") else None,
357
+ "description": str(row.get("description", "")) if row.get("description") not in (None, "") else None,
358
+ }
359
+ for row in api_rows
360
+ ]
361
+
362
+ path = _get_calendar_events_path()
363
+ if not path.exists():
364
+ return []
365
+ try:
366
+ df = pd.read_csv(path)
367
+ result = df[df["customer_id"] == customer_id]
368
+ return [
369
+ {
370
+ "id": str(row["id"]),
371
+ "customer_id": str(row["customer_id"]),
372
+ "start_time_iso": str(row["start_time_iso"]) if pd.notna(row["start_time_iso"]) else None,
373
+ "end_time_iso": str(row["end_time_iso"]) if pd.notna(row["end_time_iso"]) else None,
374
+ "title": str(row["title"]) if pd.notna(row["title"]) else None,
375
+ "description": str(row["description"]) if pd.notna(row["description"]) else None,
376
+ }
377
+ for _, row in result.iterrows()
378
+ ]
379
+ except Exception:
380
+ return []
381
+
382
+
383
+ def _get_tickets_path() -> Path:
384
+ root = Path(__file__).resolve().parents[3]
385
+ p = root / "envs" / "telecom" / "data" / "tickets" / "tickets.csv"
386
+ if not p.exists():
387
+ p = Path(__file__).parent.parent.parent / "envs" / "telecom" / "data" / "tickets" / "tickets.csv"
388
+ return p
389
+
390
+
391
+ def get_tickets_by_customer_id(customer_id: str) -> List[Dict[str, Any]]:
392
+ """Get all tickets for a customer (tickets.csv)."""
393
+ api_rows = _api_get_results("query_tickets_by_customer", {"customer_id": customer_id})
394
+ if api_rows is not None:
395
+ return [
396
+ {
397
+ "id": str(row.get("id", "")),
398
+ "customer_id": str(row.get("customer_id", customer_id)),
399
+ "issue": str(row.get("issue", "")) if row.get("issue") not in (None, "") else None,
400
+ "created_at_iso": str(row.get("created_at_iso", "")) if row.get("created_at_iso") not in (None, "") else None,
401
+ "status": str(row.get("status", "")) if row.get("status") not in (None, "") else None,
402
+ }
403
+ for row in api_rows
404
+ ]
405
+
406
+ path = _get_tickets_path()
407
+ if not path.exists():
408
+ return []
409
+ try:
410
+ df = pd.read_csv(path)
411
+ result = df[df["customer_id"] == customer_id]
412
+ return [
413
+ {
414
+ "id": str(row["id"]),
415
+ "customer_id": str(row["customer_id"]),
416
+ "issue": str(row["issue"]) if pd.notna(row["issue"]) else None,
417
+ "created_at_iso": str(row["created_at_iso"]) if pd.notna(row["created_at_iso"]) else None,
418
+ "status": str(row["status"]) if pd.notna(row["status"]) else None,
419
+ }
420
+ for _, row in result.iterrows()
421
+ ]
422
+ except Exception:
423
+ return []
424
+
425
+
426
+ def get_ticket_by_id(ticket_id: str) -> Optional[Dict[str, Any]]:
427
+ """Get a single ticket by its ID (tickets.csv). Returns None if not found."""
428
+ api_row = _api_first("query_ticket", {"id": ticket_id})
429
+ if api_row is not None:
430
+ return {
431
+ "id": str(api_row.get("id", ticket_id)),
432
+ "customer_id": str(api_row.get("customer_id", "")),
433
+ "issue": str(api_row.get("issue", "")) if api_row.get("issue") not in (None, "") else None,
434
+ "created_at_iso": str(api_row.get("created_at_iso", "")) if api_row.get("created_at_iso") not in (None, "") else None,
435
+ "status": str(api_row.get("status", "")) if api_row.get("status") not in (None, "") else None,
436
+ }
437
+
438
+ path = _get_tickets_path()
439
+ if not path.exists():
440
+ return None
441
+ try:
442
+ df = pd.read_csv(path)
443
+ result = df[df["id"].astype(str) == str(ticket_id)]
444
+ if result.empty:
445
+ return None
446
+ row = result.iloc[0]
447
+ return {
448
+ "id": str(row["id"]),
449
+ "customer_id": str(row["customer_id"]),
450
+ "issue": str(row["issue"]) if pd.notna(row["issue"]) else None,
451
+ "created_at_iso": str(row["created_at_iso"]) if pd.notna(row["created_at_iso"]) else None,
452
+ "status": str(row["status"]) if pd.notna(row["status"]) else None,
453
+ }
454
+ except Exception:
455
+ return None
456
+
457
+
458
+ def _get_orders_path() -> Path:
459
+ root = Path(__file__).resolve().parents[3]
460
+ p = root / "envs" / "telecom" / "data" / "orders" / "orders.csv"
461
+ if not p.exists():
462
+ p = Path(__file__).parent.parent.parent / "envs" / "telecom" / "data" / "orders" / "orders.csv"
463
+ return p
464
+
465
+
466
+ def get_order_by_id(order_id: str) -> Optional[Dict[str, Any]]:
467
+ """Get a single order by its ID (orders.csv). Returns None if not found. Includes refunded and damaged_or_altered."""
468
+ api_row = _api_first("query_order", {"order_id": order_id})
469
+ if api_row is not None:
470
+ out = {
471
+ "order_id": str(api_row.get("order_id", order_id)),
472
+ "customer_id": str(api_row.get("customer_id", "")),
473
+ "refunded": _as_bool(api_row.get("refunded")),
474
+ }
475
+ if api_row.get("item") not in (None, ""):
476
+ out["item"] = str(api_row.get("item"))
477
+ if "damaged_or_altered" in api_row:
478
+ out["damaged_or_altered"] = _as_bool(api_row.get("damaged_or_altered"))
479
+ return out
480
+
481
+ path = _get_orders_path()
482
+ if not path.exists():
483
+ return None
484
+ try:
485
+ df = pd.read_csv(path)
486
+ result = df[df["order_id"].astype(str) == str(order_id)]
487
+ if result.empty:
488
+ return None
489
+ row = result.iloc[0]
490
+ out = {
491
+ "order_id": str(row["order_id"]),
492
+ "customer_id": str(row["customer_id"]),
493
+ "refunded": str(row["refunded"]).lower() == "true" if "refunded" in row and pd.notna(row.get("refunded")) else False,
494
+ }
495
+ if "item" in row and pd.notna(row.get("item")):
496
+ out["item"] = str(row["item"])
497
+ if "damaged_or_altered" in row:
498
+ out["damaged_or_altered"] = str(row["damaged_or_altered"]).lower() == "true"
499
+ return out
500
+ except Exception:
501
+ return None
502
+
503
+
504
+ def _get_sms_path() -> Path:
505
+ root = Path(__file__).resolve().parents[3]
506
+ p = root / "envs" / "telecom" / "data" / "sms" / "sms.csv"
507
+ if not p.exists():
508
+ p = Path(__file__).parent.parent.parent / "envs" / "telecom" / "data" / "sms" / "sms.csv"
509
+ return p
510
+
511
+
512
+ def get_sms_by_customer_id(customer_id: str) -> List[Dict[str, Any]]:
513
+ """Get all SMS records for a customer (sms.csv)."""
514
+ api_rows = _api_get_results("query_sms_by_customer", {"customer_id": customer_id})
515
+ if api_rows is not None:
516
+ return [
517
+ {
518
+ "id": str(row.get("id", "")),
519
+ "customer_id": str(row.get("customer_id", customer_id)),
520
+ "body": str(row.get("body", "")) if row.get("body") not in (None, "") else None,
521
+ "sent_at_iso": str(row.get("sent_at_iso", "")) if row.get("sent_at_iso") not in (None, "") else None,
522
+ }
523
+ for row in api_rows
524
+ ]
525
+
526
+ path = _get_sms_path()
527
+ if not path.exists():
528
+ return []
529
+ try:
530
+ df = pd.read_csv(path)
531
+ result = df[df["customer_id"] == customer_id]
532
+ return [
533
+ {
534
+ "id": str(row["id"]),
535
+ "customer_id": str(row["customer_id"]),
536
+ "body": str(row["body"]) if pd.notna(row["body"]) else None,
537
+ "sent_at_iso": str(row["sent_at_iso"]) if pd.notna(row["sent_at_iso"]) else None,
538
+ }
539
+ for _, row in result.iterrows()
540
+ ]
541
+ except Exception:
542
+ return []
543
+
544
+
545
+ def _get_emails_path() -> Path:
546
+ root = Path(__file__).resolve().parents[3]
547
+ p = root / "envs" / "telecom" / "data" / "emails" / "emails.csv"
548
+ if not p.exists():
549
+ p = Path(__file__).parent.parent.parent / "envs" / "telecom" / "data" / "emails" / "emails.csv"
550
+ return p
551
+
552
+
553
+ def get_emails_by_customer_id(customer_id: str) -> List[Dict[str, Any]]:
554
+ """Get all email records for a customer (emails.csv)."""
555
+ api_rows = _api_get_results("query_emails_by_customer", {"customer_id": customer_id})
556
+ if api_rows is not None:
557
+ return [
558
+ {
559
+ "id": str(row.get("id", "")),
560
+ "customer_id": str(row.get("customer_id", customer_id)),
561
+ "subject": str(row.get("subject", "")) if row.get("subject") not in (None, "") else None,
562
+ "body": str(row.get("body", "")) if row.get("body") not in (None, "") else None,
563
+ "sent_at_iso": str(row.get("sent_at_iso", "")) if row.get("sent_at_iso") not in (None, "") else None,
564
+ }
565
+ for row in api_rows
566
+ ]
567
+
568
+ path = _get_emails_path()
569
+ if not path.exists():
570
+ return []
571
+ try:
572
+ df = pd.read_csv(path)
573
+ result = df[df["customer_id"] == customer_id]
574
+ return [
575
+ {
576
+ "id": str(row["id"]),
577
+ "customer_id": str(row["customer_id"]),
578
+ "subject": str(row["subject"]) if pd.notna(row["subject"]) else None,
579
+ "body": str(row["body"]) if pd.notna(row["body"]) else None,
580
+ "sent_at_iso": str(row["sent_at_iso"]) if pd.notna(row["sent_at_iso"]) else None,
581
+ }
582
+ for _, row in result.iterrows()
583
+ ]
584
+ except Exception:
585
+ return []
586
+
587
+
588
+ def _get_accounts_path() -> Path:
589
+ """Get the path to the accounts CSV file."""
590
+ # Try to resolve relative to dt_arena root
591
+ root = Path(__file__).resolve().parents[3]
592
+ accounts_path = root / "envs" / "telecom" / "data" / "accounts" / "accounts.csv"
593
+
594
+ if not accounts_path.exists():
595
+ # Fallback: try relative to current file
596
+ accounts_path = Path(__file__).parent.parent.parent / "envs" / "telecom" / "data" / "accounts" / "accounts.csv"
597
+
598
+ return accounts_path
599
+
600
+
601
+ def _get_forum_threads_path() -> Path:
602
+ root = Path(__file__).resolve().parents[3]
603
+ p = root / "envs" / "telecom" / "data" / "forum_threads" / "forum_threads.csv"
604
+ if not p.exists():
605
+ p = Path(__file__).parent.parent.parent / "envs" / "telecom" / "data" / "forum_threads" / "forum_threads.csv"
606
+ return p
607
+
608
+
609
+ def _get_forum_posts_path() -> Path:
610
+ root = Path(__file__).resolve().parents[3]
611
+ p = root / "envs" / "telecom" / "data" / "forum_posts" / "forum_posts.csv"
612
+ if not p.exists():
613
+ p = Path(__file__).parent.parent.parent / "envs" / "telecom" / "data" / "forum_posts" / "forum_posts.csv"
614
+ return p
615
+
616
+
617
+ def get_forum_thread_by_id(thread_id: str) -> Optional[Dict[str, Any]]:
618
+ """Get a forum thread by ID."""
619
+ api_row = _api_first("query_forum_thread", {"id": thread_id})
620
+ if api_row is not None:
621
+ return {
622
+ "id": str(api_row.get("id", thread_id)),
623
+ "title": str(api_row.get("title", "")),
624
+ "topic": str(api_row.get("topic", "")),
625
+ "created_at_iso": str(api_row.get("created_at_iso", "")),
626
+ "status": str(api_row.get("status", "")),
627
+ "author_id": str(api_row.get("author_id", "")),
628
+ "author_type": str(api_row.get("author_type", "")),
629
+ }
630
+
631
+ path = _get_forum_threads_path()
632
+ if not path.exists():
633
+ return None
634
+ try:
635
+ df = pd.read_csv(path)
636
+ result = df[df["id"].astype(str) == str(thread_id)]
637
+ if result.empty:
638
+ return None
639
+ row = result.iloc[0]
640
+ return {
641
+ "id": str(row["id"]),
642
+ "title": str(row["title"]) if pd.notna(row.get("title")) else "",
643
+ "topic": str(row["topic"]) if pd.notna(row.get("topic")) else "",
644
+ "created_at_iso": str(row["created_at_iso"]) if pd.notna(row.get("created_at_iso")) else "",
645
+ "status": str(row["status"]) if pd.notna(row.get("status")) else "",
646
+ "author_id": str(row["author_id"]) if pd.notna(row.get("author_id")) else "",
647
+ "author_type": str(row["author_type"]) if pd.notna(row.get("author_type")) else "",
648
+ }
649
+ except Exception:
650
+ return None
651
+
652
+
653
+ def get_forum_posts_by_thread_id(thread_id: str) -> List[Dict[str, Any]]:
654
+ """Get all forum posts in a thread."""
655
+ api_rows = _api_get_results("query_forum_posts_by_thread", {"thread_id": thread_id})
656
+ if api_rows is not None:
657
+ return [
658
+ {
659
+ "id": str(row.get("id", "")),
660
+ "thread_id": str(row.get("thread_id", thread_id)),
661
+ "author_id": str(row.get("author_id", "")),
662
+ "author_type": str(row.get("author_type", "")),
663
+ "content": str(row.get("content", "")),
664
+ "created_at_iso": str(row.get("created_at_iso", "")),
665
+ }
666
+ for row in api_rows
667
+ ]
668
+
669
+ path = _get_forum_posts_path()
670
+ if not path.exists():
671
+ return []
672
+ try:
673
+ df = pd.read_csv(path)
674
+ result = df[df["thread_id"].astype(str) == str(thread_id)].sort_values("created_at_iso")
675
+ return [
676
+ {
677
+ "id": str(row["id"]),
678
+ "thread_id": str(row["thread_id"]),
679
+ "author_id": str(row["author_id"]) if pd.notna(row.get("author_id")) else "",
680
+ "author_type": str(row["author_type"]) if pd.notna(row.get("author_type")) else "",
681
+ "content": str(row["content"]) if pd.notna(row.get("content")) else "",
682
+ "created_at_iso": str(row["created_at_iso"]) if pd.notna(row.get("created_at_iso")) else "",
683
+ }
684
+ for _, row in result.iterrows()
685
+ ]
686
+ except Exception:
687
+ return []
688
+
689
+
690
+ def get_forum_post_by_id(post_id: str) -> Optional[Dict[str, Any]]:
691
+ """Get a forum post by ID."""
692
+ api_row = _api_first("query_forum_post", {"id": post_id})
693
+ if api_row is not None:
694
+ return {
695
+ "id": str(api_row.get("id", post_id)),
696
+ "thread_id": str(api_row.get("thread_id", "")),
697
+ "author_id": str(api_row.get("author_id", "")),
698
+ "author_type": str(api_row.get("author_type", "")),
699
+ "content": str(api_row.get("content", "")),
700
+ "created_at_iso": str(api_row.get("created_at_iso", "")),
701
+ }
702
+
703
+ path = _get_forum_posts_path()
704
+ if not path.exists():
705
+ return None
706
+ try:
707
+ df = pd.read_csv(path)
708
+ result = df[df["id"].astype(str) == str(post_id)]
709
+ if result.empty:
710
+ return None
711
+ row = result.iloc[0]
712
+ return {
713
+ "id": str(row["id"]),
714
+ "thread_id": str(row["thread_id"]),
715
+ "author_id": str(row["author_id"]) if pd.notna(row.get("author_id")) else "",
716
+ "author_type": str(row["author_type"]) if pd.notna(row.get("author_type")) else "",
717
+ "content": str(row["content"]) if pd.notna(row.get("content")) else "",
718
+ "created_at_iso": str(row["created_at_iso"]) if pd.notna(row.get("created_at_iso")) else "",
719
+ }
720
+ except Exception:
721
+ return None
722
+
723
+
724
+ def get_account_by_customer_id(customer_id: str) -> Optional[Dict[str, Any]]:
725
+ """Get account information by customer ID.
726
+
727
+ Args:
728
+ customer_id: The customer ID to query
729
+
730
+ Returns:
731
+ Dictionary with account information (customer_id, account_name, password) or None if not found
732
+ """
733
+ api_row = _api_first("query_account", {"customer_id": customer_id})
734
+ if api_row is not None:
735
+ return {
736
+ "customer_id": str(api_row.get("customer_id", customer_id)),
737
+ "account_name": str(api_row.get("account_name", "")),
738
+ "password": str(api_row.get("password", "")),
739
+ }
740
+
741
+ accounts_path = _get_accounts_path()
742
+
743
+ if not accounts_path.exists():
744
+ return None
745
+
746
+ try:
747
+ df = pd.read_csv(accounts_path)
748
+ result = df[df["customer_id"] == customer_id]
749
+
750
+ if len(result) == 0:
751
+ return None
752
+
753
+ row = result.iloc[0]
754
+ return {
755
+ "customer_id": str(row["customer_id"]),
756
+ "account_name": str(row["account_name"]),
757
+ "password": str(row["password"])
758
+ }
759
+ except Exception:
760
+ return None