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,762 @@
1
+ """
2
+ CourtListener Data Store — Local mock of CourtListener API v4.3.
3
+
4
+ Loads data fetched from CourtListener REST API and provides methods
5
+ that mirror each endpoint's behavior (filtering, pagination, lookup).
6
+
7
+ All list endpoints return paginated format:
8
+ {"count": N, "next": null, "previous": null, "results": [...]}
9
+
10
+ All detail endpoints return the object directly (or {"detail": "Not found."} on 404).
11
+ """
12
+
13
+ import json
14
+ import re
15
+ from pathlib import Path
16
+ from typing import Any, Dict, List, Optional
17
+
18
+
19
+ class CourtListenerStore:
20
+ """Local mock of CourtListener API v4.3 data store."""
21
+
22
+ def __init__(self, data_dir: str):
23
+ self.data_dir = Path(data_dir) / "courtlistener"
24
+
25
+ # Core data stores (keyed by ID)
26
+ self._clusters: Dict[int, dict] = {}
27
+ self._opinions: Dict[int, dict] = {}
28
+ self._courts: Dict[str, dict] = {}
29
+ self._dockets: Dict[int, dict] = {}
30
+ self._docket_entries: Dict[int, dict] = {}
31
+ self._recap_documents: Dict[int, dict] = {}
32
+ self._audio: Dict[int, dict] = {}
33
+
34
+ # People / Judges
35
+ self._people: Dict[int, dict] = {}
36
+ self._positions: Dict[int, dict] = {}
37
+ self._educations: Dict[int, dict] = {}
38
+ self._political_affiliations: Dict[int, dict] = {}
39
+ self._aba_ratings: Dict[int, dict] = {}
40
+
41
+ # Parties / Attorneys
42
+ self._parties: Dict[int, dict] = {}
43
+ self._attorneys: Dict[int, dict] = {}
44
+
45
+ # Financial Disclosures
46
+ self._financial_disclosures: Dict[int, dict] = {}
47
+ self._investments: Dict[int, dict] = {}
48
+ self._gifts: Dict[int, dict] = {}
49
+ self._debts: Dict[int, dict] = {}
50
+ self._agreements: Dict[int, dict] = {}
51
+ self._non_investment_incomes: Dict[int, dict] = {}
52
+ self._disclosure_positions: Dict[int, dict] = {}
53
+ self._reimbursements: Dict[int, dict] = {}
54
+ self._spouse_incomes: Dict[int, dict] = {}
55
+
56
+ # Other
57
+ self._bankruptcy_info: Dict[int, dict] = {}
58
+ self._originating_court_info: Dict[int, dict] = {}
59
+ self._fjc_data: Dict[int, dict] = {}
60
+
61
+ # Opinions-cited relationships
62
+ self._opinions_cited: List[dict] = []
63
+
64
+ # Search index: query_slug -> raw search response
65
+ self._search_cache: Dict[str, dict] = {}
66
+
67
+ self._load_all()
68
+
69
+ # ==================================================================
70
+ # DATA LOADING
71
+ # ==================================================================
72
+
73
+ def _load_all(self):
74
+ """Load all data from JSON files."""
75
+ if not self.data_dir.exists():
76
+ print(f"[CourtListenerStore] Data dir not found: {self.data_dir}")
77
+ return
78
+
79
+ self._load_search_cache()
80
+ self._load_clusters()
81
+ self._load_opinions()
82
+ self._load_courts()
83
+ self._load_people()
84
+ self._load_dockets()
85
+ self._load_financial_disclosures()
86
+ self._load_audio()
87
+ self._load_other()
88
+ self._load_opinions_cited()
89
+
90
+ def _load_json_file(self, filename: str) -> Any:
91
+ """Load a JSON file from the data directory."""
92
+ filepath = self.data_dir / filename
93
+ if not filepath.exists():
94
+ return []
95
+ try:
96
+ with open(filepath) as f:
97
+ return json.load(f)
98
+ except Exception as e:
99
+ print(f"[CourtListenerStore] Error loading {filepath}: {e}")
100
+ return []
101
+
102
+ def _load_json_dir(self, dirname: str) -> Dict[int, dict]:
103
+ """Load all JSON files from a subdirectory, keyed by filename (ID)."""
104
+ dirpath = self.data_dir / dirname
105
+ result = {}
106
+ if not dirpath.exists():
107
+ return result
108
+ for filepath in dirpath.glob("*.json"):
109
+ try:
110
+ with open(filepath) as f:
111
+ data = json.load(f)
112
+ key = filepath.stem
113
+ if key.isdigit():
114
+ key = int(key)
115
+ result[key] = data
116
+ except Exception as e:
117
+ print(f"[CourtListenerStore] Error loading {filepath}: {e}")
118
+ return result
119
+
120
+ def _load_list_to_dict(self, filename: str, key_field: str = "id") -> dict:
121
+ """Load a JSON list file and index by a key field."""
122
+ data = self._load_json_file(filename)
123
+ if not isinstance(data, list):
124
+ return {}
125
+ result = {}
126
+ for item in data:
127
+ k = item.get(key_field)
128
+ if k is not None:
129
+ result[k] = item
130
+ return result
131
+
132
+ def _load_search_cache(self):
133
+ """Load search result files."""
134
+ search_dir = self.data_dir / "search_results"
135
+ if not search_dir.exists():
136
+ return
137
+ for filepath in search_dir.glob("*.json"):
138
+ slug = filepath.stem
139
+ try:
140
+ with open(filepath) as f:
141
+ self._search_cache[slug] = json.load(f)
142
+ except Exception:
143
+ pass
144
+
145
+ def _load_clusters(self):
146
+ self._clusters = self._load_json_dir("clusters")
147
+
148
+ def _load_opinions(self):
149
+ self._opinions = self._load_json_dir("opinions")
150
+
151
+ def _load_courts(self):
152
+ data = self._load_json_file("courts.json")
153
+ if isinstance(data, list):
154
+ for court in data:
155
+ cid = court.get("id")
156
+ if cid:
157
+ self._courts[cid] = court
158
+
159
+ def _load_people(self):
160
+ self._people = self._load_list_to_dict("people.json")
161
+ self._positions = self._load_list_to_dict("positions.json")
162
+ self._educations = self._load_list_to_dict("educations.json")
163
+ self._political_affiliations = self._load_list_to_dict("political_affiliations.json")
164
+ self._aba_ratings = self._load_list_to_dict("aba_ratings.json")
165
+
166
+ def _load_dockets(self):
167
+ self._dockets = self._load_list_to_dict("dockets.json")
168
+ self._docket_entries = self._load_list_to_dict("docket_entries.json")
169
+ self._parties = self._load_list_to_dict("parties.json")
170
+ self._attorneys = self._load_list_to_dict("attorneys.json")
171
+
172
+ def _load_financial_disclosures(self):
173
+ self._financial_disclosures = self._load_list_to_dict("financial_disclosures.json")
174
+ self._investments = self._load_list_to_dict("investments.json")
175
+ self._gifts = self._load_list_to_dict("gifts.json")
176
+ self._debts = self._load_list_to_dict("debts.json")
177
+ self._agreements = self._load_list_to_dict("agreements.json")
178
+ self._non_investment_incomes = self._load_list_to_dict("non_investment_incomes.json")
179
+ self._disclosure_positions = self._load_list_to_dict("disclosure_positions.json")
180
+ self._reimbursements = self._load_list_to_dict("reimbursements.json")
181
+ self._spouse_incomes = self._load_list_to_dict("spouse_incomes.json")
182
+
183
+ def _load_audio(self):
184
+ self._audio = self._load_list_to_dict("audio.json")
185
+
186
+ def _load_other(self):
187
+ self._bankruptcy_info = self._load_list_to_dict("bankruptcy_info.json")
188
+ self._originating_court_info = self._load_list_to_dict("originating_court_info.json")
189
+ self._fjc_data = self._load_list_to_dict("fjc_data.json")
190
+ self._recap_documents = self._load_list_to_dict("recap_documents.json")
191
+
192
+ def _load_opinions_cited(self):
193
+ data = self._load_json_file("opinions_cited.json")
194
+ if isinstance(data, list):
195
+ self._opinions_cited = data
196
+
197
+ # ==================================================================
198
+ # HELPERS
199
+ # ==================================================================
200
+
201
+ @staticmethod
202
+ def _paginate(items: list, page_size: int = 20, cursor: str = "") -> dict:
203
+ """Return paginated response format."""
204
+ # Simple offset-based pagination
205
+ offset = 0
206
+ if cursor:
207
+ try:
208
+ offset = int(cursor)
209
+ except ValueError:
210
+ offset = 0
211
+
212
+ total = len(items)
213
+ page_items = items[offset:offset + page_size]
214
+ next_cursor = None
215
+ if offset + page_size < total:
216
+ next_cursor = str(offset + page_size)
217
+
218
+ return {
219
+ "count": total,
220
+ "next": next_cursor,
221
+ "previous": str(max(0, offset - page_size)) if offset > 0 else None,
222
+ "results": page_items,
223
+ }
224
+
225
+ @staticmethod
226
+ def _extract_id_from_url(ref) -> int:
227
+ """Extract numeric ID from a CL API URL string or nested object dict."""
228
+ if not ref:
229
+ return 0
230
+ # If it's a dict (nested object), extract the 'id' field
231
+ if isinstance(ref, dict):
232
+ return int(ref.get("id", 0))
233
+ # If it's an int already
234
+ if isinstance(ref, int):
235
+ return ref
236
+ # Otherwise treat as URL string
237
+ url = str(ref)
238
+ parts = url.rstrip("/").split("/")
239
+ for part in reversed(parts):
240
+ if part.isdigit():
241
+ return int(part)
242
+ return 0
243
+
244
+ def _text_match(self, text: str, query: str) -> bool:
245
+ """Simple text matching for search."""
246
+ if not query:
247
+ return True
248
+ query_lower = query.lower()
249
+ words = query_lower.split()
250
+ text_lower = text.lower()
251
+ return any(w in text_lower for w in words)
252
+
253
+ # ==================================================================
254
+ # UNIFIED SEARCH (mirrors GET /search/)
255
+ # ==================================================================
256
+
257
+ def search(self, q: str, type: str = "o", court: str = "",
258
+ date_filed_after: str = "", date_filed_before: str = "",
259
+ page_size: int = 20, cursor: str = "",
260
+ order_by: str = "score desc") -> dict:
261
+ """Unified search across case law, RECAP, judges, oral arguments.
262
+
263
+ type: o (opinions), r (RECAP), rd (RECAP documents), d (dockets), p (people), oa (oral arguments)
264
+ """
265
+ if type == "o":
266
+ return self._search_opinions(q, court, date_filed_after, date_filed_before, page_size, cursor)
267
+ elif type == "d":
268
+ return self._search_dockets_internal(q, court, page_size, cursor)
269
+ elif type == "p":
270
+ return self._search_people(q, page_size, cursor)
271
+ elif type == "oa":
272
+ return self._search_audio(q, page_size, cursor)
273
+ elif type in ("r", "rd"):
274
+ return self._search_recap(q, page_size, cursor)
275
+ else:
276
+ return self._search_opinions(q, court, date_filed_after, date_filed_before, page_size, cursor)
277
+
278
+ def _search_opinions(self, q: str, court: str = "",
279
+ date_filed_after: str = "", date_filed_before: str = "",
280
+ page_size: int = 20, cursor: str = "") -> dict:
281
+ """Search opinions — first check cached search results, then fall back to cluster scan."""
282
+ # Try to find a cached search result that matches
283
+ q_slug = q.lower().replace(" ", "_")[:50]
284
+ if q_slug in self._search_cache:
285
+ cached = self._search_cache[q_slug]
286
+ results = cached.get("results", [])
287
+ # Apply filters
288
+ if court:
289
+ results = [r for r in results if court.lower() in str(r.get("court_id", "")).lower()]
290
+ if date_filed_after:
291
+ results = [r for r in results if str(r.get("dateFiled", "")) >= date_filed_after]
292
+ if date_filed_before:
293
+ results = [r for r in results if str(r.get("dateFiled", "")) <= date_filed_before]
294
+ return self._paginate(results, page_size, cursor)
295
+
296
+ # Fall back to scanning clusters
297
+ results = []
298
+ for cid, cluster in self._clusters.items():
299
+ searchable = " ".join([
300
+ str(cluster.get("case_name", "")),
301
+ str(cluster.get("case_name_full", "")),
302
+ str(cluster.get("syllabus", "")),
303
+ str(cluster.get("summary", "")),
304
+ str(cluster.get("judges", "")),
305
+ str(cluster.get("attorneys", "")),
306
+ str(cluster.get("nature_of_suit", "")),
307
+ ])
308
+
309
+ if not self._text_match(searchable, q):
310
+ continue
311
+
312
+ if court:
313
+ # Extract court_id from docket URL or cluster data
314
+ cluster_court = ""
315
+ docket_url = cluster.get("docket", "")
316
+ if isinstance(docket_url, str):
317
+ # Try to get court from linked docket
318
+ did = self._extract_id_from_url(docket_url)
319
+ if did and did in self._dockets:
320
+ cluster_court = self._dockets[did].get("court_id", "")
321
+ if court.lower() not in cluster_court.lower():
322
+ continue
323
+
324
+ date_filed = str(cluster.get("date_filed", ""))
325
+ if date_filed_after and date_filed < date_filed_after:
326
+ continue
327
+ if date_filed_before and date_filed > date_filed_before:
328
+ continue
329
+
330
+ # Build search result format
331
+ citations = cluster.get("citations", [])
332
+ citation_strs = []
333
+ for c in citations:
334
+ if isinstance(c, dict):
335
+ citation_strs.append(f"{c.get('volume', '')} {c.get('reporter', '')} {c.get('page', '')}".strip())
336
+ elif isinstance(c, str):
337
+ citation_strs.append(c)
338
+
339
+ result = {
340
+ "absolute_url": f"/opinion/{cid}/{cluster.get('slug', '')}/",
341
+ "caseName": cluster.get("case_name", ""),
342
+ "caseNameFull": cluster.get("case_name_full", ""),
343
+ "citation": citation_strs,
344
+ "citeCount": cluster.get("citation_count", 0),
345
+ "cluster_id": cid,
346
+ "court": "",
347
+ "court_id": "",
348
+ "dateFiled": date_filed,
349
+ "docketNumber": "",
350
+ "docket_id": self._extract_id_from_url(cluster.get("docket", "")),
351
+ "judge": cluster.get("judges", ""),
352
+ "snippet": (cluster.get("syllabus", "") or cluster.get("summary", ""))[:300],
353
+ }
354
+ results.append(result)
355
+
356
+ # If court filter produced no results, retry without court filter
357
+ if not results and court:
358
+ return self._search_opinions(q, court="",
359
+ date_filed_after=date_filed_after,
360
+ date_filed_before=date_filed_before,
361
+ page_size=page_size, cursor=cursor)
362
+
363
+ # Sort by date descending
364
+ results.sort(key=lambda x: x.get("dateFiled", ""), reverse=True)
365
+ return self._paginate(results, page_size, cursor)
366
+
367
+ def _search_dockets_internal(self, q: str, court: str = "",
368
+ page_size: int = 20, cursor: str = "") -> dict:
369
+ """Search dockets."""
370
+ results = []
371
+ for did, docket in self._dockets.items():
372
+ searchable = " ".join([
373
+ str(docket.get("case_name", "")),
374
+ str(docket.get("case_name_full", "")),
375
+ str(docket.get("docket_number", "")),
376
+ str(docket.get("nature_of_suit", "")),
377
+ ])
378
+ if not self._text_match(searchable, q):
379
+ continue
380
+ if court and court.lower() not in str(docket.get("court_id", "")).lower():
381
+ continue
382
+ results.append(docket)
383
+
384
+ results.sort(key=lambda x: str(x.get("date_filed", "")), reverse=True)
385
+ return self._paginate(results, page_size, cursor)
386
+
387
+ def _search_people(self, q: str, page_size: int = 20, cursor: str = "") -> dict:
388
+ """Search people/judges."""
389
+ results = []
390
+ for pid, person in self._people.items():
391
+ searchable = " ".join([
392
+ str(person.get("name_first", "")),
393
+ str(person.get("name_last", "")),
394
+ str(person.get("name_middle", "")),
395
+ ])
396
+ if not self._text_match(searchable, q):
397
+ continue
398
+ results.append(person)
399
+ return self._paginate(results, page_size, cursor)
400
+
401
+ def _search_audio(self, q: str, page_size: int = 20, cursor: str = "") -> dict:
402
+ """Search oral arguments."""
403
+ results = []
404
+ for aid, audio in self._audio.items():
405
+ searchable = " ".join([
406
+ str(audio.get("case_name", "")),
407
+ str(audio.get("judges", "")),
408
+ ])
409
+ if not self._text_match(searchable, q):
410
+ continue
411
+ results.append(audio)
412
+ return self._paginate(results, page_size, cursor)
413
+
414
+ def _search_recap(self, q: str, page_size: int = 20, cursor: str = "") -> dict:
415
+ """Search RECAP documents."""
416
+ results = []
417
+ for rid, doc in self._recap_documents.items():
418
+ searchable = str(doc.get("description", ""))
419
+ if not self._text_match(searchable, q):
420
+ continue
421
+ results.append(doc)
422
+ return self._paginate(results, page_size, cursor)
423
+
424
+ # ==================================================================
425
+ # CASE LAW
426
+ # ==================================================================
427
+
428
+ def get_cluster(self, cluster_id: int) -> dict:
429
+ """Get cluster by ID (mirrors GET /clusters/{id}/)."""
430
+ cluster = self._clusters.get(cluster_id)
431
+ if not cluster:
432
+ return {"detail": "Not found."}
433
+ return dict(cluster)
434
+
435
+ def get_opinion(self, opinion_id: int) -> dict:
436
+ """Get opinion by ID (mirrors GET /opinions/{id}/)."""
437
+ opinion = self._opinions.get(opinion_id)
438
+ if not opinion:
439
+ return {"detail": "Not found."}
440
+ return dict(opinion)
441
+
442
+ def get_opinions_cited(self, citing_opinion: int = 0, cited_opinion: int = 0,
443
+ page_size: int = 20) -> dict:
444
+ """Get citation relationships (mirrors GET /opinions-cited/)."""
445
+ results = []
446
+ for rel in self._opinions_cited:
447
+ if citing_opinion:
448
+ citing_url = rel.get("citing_opinion", "")
449
+ if self._extract_id_from_url(citing_url) != citing_opinion:
450
+ continue
451
+ if cited_opinion:
452
+ cited_url = rel.get("cited_opinion", "")
453
+ if self._extract_id_from_url(cited_url) != cited_opinion:
454
+ continue
455
+ results.append(rel)
456
+ return self._paginate(results, page_size)
457
+
458
+ def citation_lookup(self, text: str = "", volume: str = "", reporter: str = "",
459
+ page: str = "") -> list:
460
+ """Resolve citations (mirrors POST /citation-lookup/).
461
+
462
+ Returns a list of lookup results.
463
+ """
464
+ results = []
465
+
466
+ # If text is provided, try to parse citations from it
467
+ # If volume/reporter/page are provided, look up directly
468
+ if volume and reporter and page:
469
+ # Search clusters for matching citation
470
+ target = f"{volume} {reporter} {page}".strip()
471
+ for cid, cluster in self._clusters.items():
472
+ for cit in cluster.get("citations", []):
473
+ if isinstance(cit, dict):
474
+ cit_str = f"{cit.get('volume', '')} {cit.get('reporter', '')} {cit.get('page', '')}".strip()
475
+ if (str(cit.get("volume", "")) == str(volume) and
476
+ reporter.lower() in str(cit.get("reporter", "")).lower() and
477
+ str(cit.get("page", "")) == str(page)):
478
+ results.append({
479
+ "citation": cit_str,
480
+ "normalized_citations": [cit_str],
481
+ "status": 200,
482
+ "error_message": "",
483
+ "clusters": [cluster],
484
+ })
485
+ return results
486
+
487
+ # Not found
488
+ results.append({
489
+ "citation": target,
490
+ "normalized_citations": [target],
491
+ "status": 404,
492
+ "error_message": "Citation not found in database.",
493
+ "clusters": [],
494
+ })
495
+ return results
496
+
497
+ if text:
498
+ # Try to find the text as a citation string in any cluster
499
+ text_lower = text.lower().strip()
500
+ for cid, cluster in self._clusters.items():
501
+ for cit in cluster.get("citations", []):
502
+ cit_str = ""
503
+ if isinstance(cit, dict):
504
+ cit_str = f"{cit.get('volume', '')} {cit.get('reporter', '')} {cit.get('page', '')}".strip()
505
+ elif isinstance(cit, str):
506
+ cit_str = cit
507
+
508
+ if cit_str and text_lower in cit_str.lower():
509
+ results.append({
510
+ "citation": cit_str,
511
+ "normalized_citations": [cit_str],
512
+ "status": 200,
513
+ "error_message": "",
514
+ "clusters": [cluster],
515
+ })
516
+ return results
517
+
518
+ # Also check case_name
519
+ if text_lower in cluster.get("case_name", "").lower():
520
+ citations = cluster.get("citations", [])
521
+ cit_str = ""
522
+ if citations:
523
+ c = citations[0]
524
+ if isinstance(c, dict):
525
+ cit_str = f"{c.get('volume', '')} {c.get('reporter', '')} {c.get('page', '')}".strip()
526
+ elif isinstance(c, str):
527
+ cit_str = c
528
+ results.append({
529
+ "citation": cit_str or cluster.get("case_name", ""),
530
+ "normalized_citations": [cit_str] if cit_str else [],
531
+ "status": 200,
532
+ "error_message": "",
533
+ "clusters": [cluster],
534
+ })
535
+ return results
536
+
537
+ # Not found
538
+ results.append({
539
+ "citation": text,
540
+ "normalized_citations": [],
541
+ "status": 404,
542
+ "error_message": "Citation not found in database.",
543
+ "clusters": [],
544
+ })
545
+
546
+ return results
547
+
548
+ # ==================================================================
549
+ # DOCKETS & FILINGS
550
+ # ==================================================================
551
+
552
+ def get_docket(self, docket_id: int) -> dict:
553
+ """Get docket by ID (mirrors GET /dockets/{id}/)."""
554
+ docket = self._dockets.get(docket_id)
555
+ if not docket:
556
+ return {"detail": "Not found."}
557
+ return dict(docket)
558
+
559
+ def get_docket_entries(self, docket: int = 0, page_size: int = 20) -> dict:
560
+ """Get docket entries (mirrors GET /docket-entries/)."""
561
+ results = []
562
+ for eid, entry in self._docket_entries.items():
563
+ if docket:
564
+ entry_docket_url = entry.get("docket", "")
565
+ if self._extract_id_from_url(entry_docket_url) != docket:
566
+ continue
567
+ results.append(entry)
568
+ results.sort(key=lambda x: x.get("entry_number", 0))
569
+ return self._paginate(results, page_size)
570
+
571
+ def get_recap_document(self, rd_id: int) -> dict:
572
+ """Get RECAP document by ID (mirrors GET /recap-documents/{id}/)."""
573
+ doc = self._recap_documents.get(rd_id)
574
+ if not doc:
575
+ return {"detail": "Not found."}
576
+ return dict(doc)
577
+
578
+ def get_parties(self, docket: int = 0, page_size: int = 20) -> dict:
579
+ """Get parties (mirrors GET /parties/)."""
580
+ results = []
581
+ for pid, party in self._parties.items():
582
+ if docket:
583
+ party_docket_url = party.get("docket", "")
584
+ if self._extract_id_from_url(party_docket_url) != docket:
585
+ continue
586
+ results.append(party)
587
+ return self._paginate(results, page_size)
588
+
589
+ def get_attorneys(self, docket: int = 0, page_size: int = 20) -> dict:
590
+ """Get attorneys (mirrors GET /attorneys/)."""
591
+ results = []
592
+ for aid, atty in self._attorneys.items():
593
+ if docket:
594
+ atty_docket_url = atty.get("docket", "")
595
+ if self._extract_id_from_url(atty_docket_url) != docket:
596
+ continue
597
+ results.append(atty)
598
+ return self._paginate(results, page_size)
599
+
600
+ def get_bankruptcy_info(self, docket: int = 0) -> dict:
601
+ """Get bankruptcy information (mirrors GET /bankruptcy-information/)."""
602
+ results = []
603
+ for bid, info in self._bankruptcy_info.items():
604
+ if docket:
605
+ info_docket_url = info.get("docket", "")
606
+ if self._extract_id_from_url(info_docket_url) != docket:
607
+ continue
608
+ results.append(info)
609
+ return self._paginate(results, 20)
610
+
611
+ def get_originating_court_info(self, docket: int = 0) -> dict:
612
+ """Get originating court information (mirrors GET /originating-court-information/)."""
613
+ results = []
614
+ for oid, info in self._originating_court_info.items():
615
+ if docket:
616
+ info_docket_url = info.get("docket", "")
617
+ if self._extract_id_from_url(info_docket_url) != docket:
618
+ continue
619
+ results.append(info)
620
+ return self._paginate(results, 20)
621
+
622
+ def get_fjc_data(self, docket: int = 0, page_size: int = 20) -> dict:
623
+ """Get FJC integrated database records (mirrors GET /fjc-integrated-database/)."""
624
+ results = []
625
+ for fid, data in self._fjc_data.items():
626
+ if docket:
627
+ data_docket_url = data.get("docket", "")
628
+ if self._extract_id_from_url(data_docket_url) != docket:
629
+ continue
630
+ results.append(data)
631
+ return self._paginate(results, page_size)
632
+
633
+ # ==================================================================
634
+ # COURTS
635
+ # ==================================================================
636
+
637
+ def get_court(self, court_id: str) -> dict:
638
+ """Get court by ID (mirrors GET /courts/{id}/)."""
639
+ court = self._courts.get(court_id)
640
+ if not court:
641
+ return {"detail": "Not found."}
642
+ return dict(court)
643
+
644
+ # ==================================================================
645
+ # ORAL ARGUMENTS
646
+ # ==================================================================
647
+
648
+ def get_audio(self, audio_id: int) -> dict:
649
+ """Get oral argument by ID (mirrors GET /audio/{id}/)."""
650
+ audio = self._audio.get(audio_id)
651
+ if not audio:
652
+ return {"detail": "Not found."}
653
+ return dict(audio)
654
+
655
+ # ==================================================================
656
+ # PEOPLE / JUDGES
657
+ # ==================================================================
658
+
659
+ def get_person(self, person_id: int) -> dict:
660
+ """Get person by ID (mirrors GET /people/{id}/)."""
661
+ person = self._people.get(person_id)
662
+ if not person:
663
+ return {"detail": "Not found."}
664
+ return dict(person)
665
+
666
+ def get_positions(self, person: int = 0, page_size: int = 20) -> dict:
667
+ """Get judicial positions (mirrors GET /positions/)."""
668
+ results = []
669
+ for pid, pos in self._positions.items():
670
+ if person:
671
+ pos_person_url = pos.get("person", "")
672
+ if self._extract_id_from_url(pos_person_url) != person:
673
+ continue
674
+ results.append(pos)
675
+ return self._paginate(results, page_size)
676
+
677
+ def get_educations(self, person: int = 0, page_size: int = 20) -> dict:
678
+ """Get education records (mirrors GET /educations/)."""
679
+ results = []
680
+ for eid, edu in self._educations.items():
681
+ if person:
682
+ edu_person_url = edu.get("person", "")
683
+ if self._extract_id_from_url(edu_person_url) != person:
684
+ continue
685
+ results.append(edu)
686
+ return self._paginate(results, page_size)
687
+
688
+ def get_political_affiliations(self, person: int = 0, page_size: int = 20) -> dict:
689
+ """Get political affiliations (mirrors GET /political-affiliations/)."""
690
+ results = []
691
+ for paid, pa in self._political_affiliations.items():
692
+ if person:
693
+ pa_person_url = pa.get("person", "")
694
+ if self._extract_id_from_url(pa_person_url) != person:
695
+ continue
696
+ results.append(pa)
697
+ return self._paginate(results, page_size)
698
+
699
+ def get_aba_ratings(self, person: int = 0, page_size: int = 20) -> dict:
700
+ """Get ABA ratings (mirrors GET /aba-ratings/)."""
701
+ results = []
702
+ for rid, rating in self._aba_ratings.items():
703
+ if person:
704
+ rating_person_url = rating.get("person", "")
705
+ if self._extract_id_from_url(rating_person_url) != person:
706
+ continue
707
+ results.append(rating)
708
+ return self._paginate(results, page_size)
709
+
710
+ # ==================================================================
711
+ # FINANCIAL DISCLOSURES
712
+ # ==================================================================
713
+
714
+ def get_financial_disclosures(self, person: int = 0, year: int = 0,
715
+ page_size: int = 20) -> dict:
716
+ """Get financial disclosures (mirrors GET /financial-disclosures/)."""
717
+ results = []
718
+ for fid, disc in self._financial_disclosures.items():
719
+ if person:
720
+ disc_person_url = disc.get("person", "")
721
+ if self._extract_id_from_url(disc_person_url) != person:
722
+ continue
723
+ if year and disc.get("year") != year:
724
+ continue
725
+ results.append(disc)
726
+ return self._paginate(results, page_size)
727
+
728
+ def _filter_by_financial_disclosure(self, store: dict, financial_disclosure: int = 0,
729
+ page_size: int = 20) -> dict:
730
+ """Generic filter for financial disclosure sub-tables."""
731
+ results = []
732
+ for iid, item in store.items():
733
+ if financial_disclosure:
734
+ fd_url = item.get("financial_disclosure", "")
735
+ if self._extract_id_from_url(fd_url) != financial_disclosure:
736
+ continue
737
+ results.append(item)
738
+ return self._paginate(results, page_size)
739
+
740
+ def get_investments(self, financial_disclosure: int = 0, page_size: int = 20) -> dict:
741
+ return self._filter_by_financial_disclosure(self._investments, financial_disclosure, page_size)
742
+
743
+ def get_gifts(self, financial_disclosure: int = 0, page_size: int = 20) -> dict:
744
+ return self._filter_by_financial_disclosure(self._gifts, financial_disclosure, page_size)
745
+
746
+ def get_debts(self, financial_disclosure: int = 0, page_size: int = 20) -> dict:
747
+ return self._filter_by_financial_disclosure(self._debts, financial_disclosure, page_size)
748
+
749
+ def get_agreements(self, financial_disclosure: int = 0, page_size: int = 20) -> dict:
750
+ return self._filter_by_financial_disclosure(self._agreements, financial_disclosure, page_size)
751
+
752
+ def get_non_investment_incomes(self, financial_disclosure: int = 0, page_size: int = 20) -> dict:
753
+ return self._filter_by_financial_disclosure(self._non_investment_incomes, financial_disclosure, page_size)
754
+
755
+ def get_disclosure_positions(self, financial_disclosure: int = 0, page_size: int = 20) -> dict:
756
+ return self._filter_by_financial_disclosure(self._disclosure_positions, financial_disclosure, page_size)
757
+
758
+ def get_reimbursements(self, financial_disclosure: int = 0, page_size: int = 20) -> dict:
759
+ return self._filter_by_financial_disclosure(self._reimbursements, financial_disclosure, page_size)
760
+
761
+ def get_spouse_incomes(self, financial_disclosure: int = 0, page_size: int = 20) -> dict:
762
+ return self._filter_by_financial_disclosure(self._spouse_incomes, financial_disclosure, page_size)