This view is limited to 50 files because it contains too many changes. See the raw diff here.
Files changed (50) hide show
  1. scratch/osint/Dockerfile +0 -27
  2. scratch/osint/README.md +0 -262
  3. scratch/osint/artifacts/leaderboard.json +0 -453
  4. scratch/osint/artifacts/osint_dashboard.html +0 -0
  5. scratch/osint/config/seed_example.json +0 -52
  6. scratch/osint/config/seed_ollama_smoke.json +0 -51
  7. scratch/osint/config/shared_config.json +0 -55
  8. scratch/osint/datasets/fixed_levels/README.md +0 -64
  9. scratch/osint/datasets/fixed_levels/complete_dataset_qwen_generated.json +0 -0
  10. scratch/osint/datasets/fixed_levels/fixed_graph_questions.json +0 -0
  11. scratch/osint/datasets/fixed_levels/leaderboard_fixed_levels.json +0 -1229
  12. scratch/osint/datasets/fixed_levels/qwen_swarm_benchmark_fixed_levels.json +0 -69
  13. scratch/osint/datasets/fixed_levels/qwen_swarm_eval_by_difficulty.json +0 -53
  14. scratch/osint/datasets/fixed_levels/qwen_swarm_eval_fixed_levels.json +0 -25
  15. scratch/osint/datasets/fixed_levels/seed_fixed_levels.json +0 -0
  16. scratch/osint/datasets/fixed_levels/shared_config_fixed_levels.json +0 -55
  17. scratch/osint/docs/reward_design_notes.md +0 -94
  18. scratch/osint/inference.py +0 -509
  19. scratch/osint/my_env_v4.py +0 -46
  20. scratch/osint/openenv.yaml +0 -66
  21. scratch/osint/pyproject.toml +0 -35
  22. scratch/osint/requirements.txt +0 -6
  23. scratch/osint/scripts/build_fixed_levels_dataset.py +0 -197
  24. scratch/osint/scripts/generate_fixed_levels_seed.py +0 -109
  25. scratch/osint/scripts/run_openai_baseline.py +0 -59
  26. scratch/osint/scripts/validate_release.py +0 -21
  27. scratch/osint/server.py +0 -564
  28. scratch/osint/server/app.py +0 -26
  29. scratch/osint/src/osint_env/__init__.py +0 -5
  30. scratch/osint/src/osint_env/agents/__init__.py +0 -7
  31. scratch/osint/src/osint_env/agents/single_agent.py +0 -41
  32. scratch/osint/src/osint_env/agents/swarm_agent.py +0 -209
  33. scratch/osint/src/osint_env/api/__init__.py +0 -19
  34. scratch/osint/src/osint_env/api/models.py +0 -73
  35. scratch/osint/src/osint_env/baselines/__init__.py +0 -4
  36. scratch/osint/src/osint_env/baselines/openai_runner.py +0 -533
  37. scratch/osint/src/osint_env/cli.py +0 -339
  38. scratch/osint/src/osint_env/config/__init__.py +0 -9
  39. scratch/osint/src/osint_env/config/shared.py +0 -248
  40. scratch/osint/src/osint_env/data/__init__.py +0 -2
  41. scratch/osint/src/osint_env/data/generator.py +0 -819
  42. scratch/osint/src/osint_env/domain/__init__.py +0 -2
  43. scratch/osint/src/osint_env/domain/models.py +0 -185
  44. scratch/osint/src/osint_env/env/__init__.py +0 -2
  45. scratch/osint/src/osint_env/env/environment.py +0 -260
  46. scratch/osint/src/osint_env/env/openenv_compat.py +0 -20
  47. scratch/osint/src/osint_env/env/reward.py +0 -483
  48. scratch/osint/src/osint_env/env/spawn_reward_hooks.py +0 -93
  49. scratch/osint/src/osint_env/eval/__init__.py +0 -2
  50. scratch/osint/src/osint_env/eval/leaderboard.py +0 -83
scratch/osint/Dockerfile DELETED
@@ -1,27 +0,0 @@
1
- FROM python:3.12-slim
2
-
3
- RUN useradd -m -u 1000 user
4
-
5
- USER user
6
- ENV HOME=/home/user \
7
- PATH=/home/user/.local/bin:$PATH \
8
- PYTHONDONTWRITEBYTECODE=1 \
9
- PYTHONUNBUFFERED=1 \
10
- PORT=7860
11
-
12
- WORKDIR $HOME/app
13
-
14
- COPY --chown=user pyproject.toml README.md openenv.yaml inference.py $HOME/app/
15
- COPY --chown=user src $HOME/app/src
16
- COPY --chown=user config $HOME/app/config
17
- COPY --chown=user datasets $HOME/app/datasets
18
- COPY --chown=user docs $HOME/app/docs
19
- COPY --chown=user scripts $HOME/app/scripts
20
- COPY --chown=user server.py $HOME/app/server.py
21
-
22
- RUN pip install --no-cache-dir --upgrade pip && \
23
- pip install --no-cache-dir -e .
24
-
25
- EXPOSE 7860
26
-
27
- CMD ["sh", "-c", "uvicorn server:app --host 0.0.0.0 --port ${PORT:-7860}"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scratch/osint/README.md DELETED
@@ -1,262 +0,0 @@
1
- ---
2
- title: OSINT OpenEnv
3
- emoji: 🕵️
4
- colorFrom: blue
5
- colorTo: yellow
6
- sdk: docker
7
- app_port: 7860
8
- pinned: false
9
- license: apache-2.0
10
- tags:
11
- - openenv
12
- - osint
13
- - benchmark
14
- - docker
15
- - fastapi
16
- short_description: Docker OSINT benchmark with fixed OpenEnv tasks.
17
- ---
18
-
19
- # OSINT OpenEnv
20
-
21
- OSINT OpenEnv is a synthetic benchmark environment for tool-using agents that must recover identities, trace events, and link entities across noisy multi-platform records. The project is designed to feel like a compact OSINT workflow rather than a raw QA dataset: agents query mock profiles, posts, forum threads, and semantic memory, build a working graph, and then submit an answer.
22
-
23
- The motivation is to provide a reproducible OpenEnv-compatible environment for evaluating graph-building and tool-using reasoning without depending on live web data, unstable APIs, or private corpora. That makes it useful for local development, regression testing, and hosted demos such as a Docker-based Hugging Face Space.
24
-
25
- ## Environment Summary
26
-
27
- The environment generates or loads a hidden canonical graph of users, aliases, organizations, locations, posts, threads, and events. It then exposes partial platform views and a task list drawn from that graph.
28
-
29
- The default hosted Space uses the fixed-level benchmark in `datasets/fixed_levels/seed_fixed_levels.json`, which now contains 30 stable tasks over a larger shared seeded graph.
30
-
31
- ## Action Space
32
-
33
- The environment exposes three actions:
34
-
35
- - `CALL_TOOL`: query platform views or semantic memory such as `search_posts`, `get_profile`, `search_threads`, `get_connections`, or `search_memory`.
36
- - `ADD_EDGE`: add a candidate relation to the working memory graph.
37
- - `ANSWER`: submit the final answer as an exact node id string.
38
-
39
- ## Observation Space
40
-
41
- Each step returns a JSON observation with four parts:
42
-
43
- - `tool_outputs`: the most recent tool results.
44
- - `graph_snapshot`: the current working-memory graph edges.
45
- - `action_history`: recent actions and rewards.
46
- - `task`: the active task id, task type, and question.
47
-
48
- ## Task Types And Difficulty
49
-
50
- The benchmark mixes direct lookups with multi-hop traces:
51
-
52
- - Easy: single-hop identity resolution, organization lookup, event lookup, or location lookup.
53
- - Mid: two-hop alias-to-user-to-organization or thread-to-event-to-user traces.
54
- - High: cross-platform multi-hop traces combining aliases, authored content, event references, organization links, and direct connections.
55
-
56
- Common task families include:
57
-
58
- - `identity_resolution`
59
- - `network_discovery`
60
- - `event_tracing`
61
- - `cross_platform_linking`
62
- - `deanonymization`
63
- - `convoluted_trace`
64
-
65
- Expected difficulty increases with the number of relations the agent must chain together and whether the evidence is split across posts, threads, aliases, and profile edges.
66
-
67
- ## Repository Layout
68
-
69
- ```text
70
- src/osint_env/
71
- agents/ single-agent and swarm runners
72
- baselines/ reusable OpenAI baseline runner
73
- config/ shared config and seed loading
74
- data/ graph/view/task generation
75
- domain/ dataclasses and environment models
76
- env/ environment, reward logic, OpenEnv compatibility shim
77
- eval/ evaluation metrics and leaderboard helpers
78
- llm/ mock, Ollama, and OpenAI client wrappers
79
- memory/ working graph and semantic memory
80
- platforms/ tool APIs over synthetic platform views
81
- viz/ dashboard export
82
-
83
- scripts/
84
- build_fixed_levels_dataset.py
85
- run_openai_baseline.py
86
-
87
- datasets/fixed_levels/
88
- seed_fixed_levels.json
89
- shared_config_fixed_levels.json
90
- qwen_swarm_benchmark_fixed_levels.json
91
-
92
- server.py FastAPI app for local use and Docker/HF Spaces
93
- Dockerfile Container entrypoint for Hugging Face Docker Spaces
94
- ```
95
-
96
- ## Setup
97
-
98
- Python 3.10+ is required.
99
-
100
- Local install:
101
-
102
- ```bash
103
- python -m pip install -e .
104
- ```
105
-
106
- Run tests:
107
-
108
- ```bash
109
- python -m pytest -q
110
- ```
111
-
112
- Run the automated release gate:
113
-
114
- ```bash
115
- python scripts/validate_release.py
116
- ```
117
-
118
- ## Usage
119
-
120
- Run one demo episode:
121
-
122
- ```bash
123
- osint-env demo --agent-mode swarm --llm-provider mock
124
- ```
125
-
126
- Run a quick evaluation:
127
-
128
- ```bash
129
- osint-env eval --episodes 5 --agent-mode swarm --llm-provider mock
130
- ```
131
-
132
- Export a dashboard:
133
-
134
- ```bash
135
- osint-env benchmark --episodes 5 --agent-mode swarm --llm-provider mock --name quick_check
136
- ```
137
-
138
- ## OpenAI Baseline
139
-
140
- The reproducible OpenAI baseline is implemented in `scripts/run_openai_baseline.py`. It runs on the fixed-level benchmark, uses a stable seeded graph/task set, writes a JSON artifact, appends a leaderboard record, and exports a dashboard.
141
-
142
- Default behavior:
143
-
144
- - dataset: fixed-level benchmark
145
- - episodes: 30
146
- - max steps per episode: 8
147
- - temperature: 0.0
148
- - output artifact: `artifacts/baselines/openai_fixed_levels_latest.json`
149
-
150
- Run it with an API key:
151
-
152
- ```bash
153
- export OPENAI_API_KEY="your_key_here"
154
- python scripts/run_openai_baseline.py --model gpt-5-nano
155
- ```
156
-
157
- The script is designed to stay bounded enough for a normal benchmark pass to finish comfortably under 20 minutes on a lightweight chat model, while still using the full fixed task set. For repeatability it fixes the benchmark graph/tasks and uses deterministic decoding settings. Because remote model backends can still change over time, the output artifact also records model metadata and system fingerprints when available.
158
-
159
- ## Inference Script
160
-
161
- The submission-ready inference entrypoint is the root `inference.py` file. It talks to the deployed Hugging Face Space over HTTP, uses the OpenAI client for all model calls, and emits structured stdout logs in the `[START]`, `[STEP]`, and `[END]` format.
162
-
163
- The script accepts `HF_TOKEN` as the primary auth variable and also supports `OPENAI_API_KEY` or `API_KEY` as local fallbacks.
164
- After a successful run, `inference.py` also posts the evaluation summary back to the Space so the latest `/dashboard` view reflects that run.
165
-
166
- Required environment variables:
167
-
168
- - `API_BASE_URL`
169
- - `MODEL_NAME`
170
- - `HF_TOKEN`
171
-
172
- Optional environment variables:
173
-
174
- - `SPACE_URL` default: `https://siddeshwar1625-osint.hf.space`
175
- - `TASK_INDICES` default: `0,10,20`
176
- - `MAX_STEPS` default: `8`
177
-
178
- Example local test command against a running local server:
179
-
180
- ```bash
181
- API_BASE_URL=https://api.openai.com/v1 MODEL_NAME=gpt-5.4-mini OPENAI_API_KEY=your_key SPACE_URL=http://127.0.0.1:7860 python inference.py
182
- ```
183
-
184
- Example test command against the deployed Space:
185
-
186
- ```bash
187
- API_BASE_URL=https://api.openai.com/v1 MODEL_NAME=gpt-5.4-mini OPENAI_API_KEY=your_key SPACE_URL=https://siddeshwar1625-osint.hf.space python inference.py
188
- ```
189
-
190
- ## Docker And Hugging Face Space
191
-
192
- The repository is ready for a Docker-based Hugging Face Space:
193
-
194
- - `README.md` includes `sdk: docker`
195
- - `README.md` includes the `openenv` Space tag
196
- - `Dockerfile` serves `server.py` on port `7860`
197
-
198
- Local Docker smoke test:
199
-
200
- ```bash
201
- docker build -t osint-openenv .
202
- docker run --rm -p 7860:7860 osint-openenv
203
- ```
204
-
205
- Then open `http://localhost:7860`.
206
-
207
- The FastAPI app serves:
208
-
209
- - `/`: overview page
210
- - `/dashboard`: generated benchmark dashboard
211
- - `/api/environment`: environment metadata
212
- - `/health`: health check (validator-friendly alias)
213
- - `/healthz`: health check (legacy alias)
214
- - `/openenv.yaml`: OpenEnv HTTP spec stub
215
- - `/openenv/tasks`: task enumeration
216
- - `/reset` and `/openenv/reset`: episode reset endpoints
217
- - `/step` and `/openenv/step`: episode step endpoints
218
- - `/state` and `/openenv/state/{session_id}`: session state endpoints (`/state` returns the latest session)
219
-
220
- ## Automated Validation
221
-
222
- The repository includes a pass/fail validation gate for the core delivery requirements:
223
-
224
- - Hugging Face Space readiness
225
- - OpenEnv spec compliance
226
- - reproducible baseline behavior
227
- - at least 3 fixed tasks with working graders
228
- - Docker image build in CI
229
-
230
- Local gate:
231
-
232
- ```bash
233
- python scripts/validate_release.py
234
- ```
235
-
236
- CI gate:
237
-
238
- - `.github/workflows/validation.yml`
239
- - runs `pytest`
240
- - runs the validation script
241
- - runs `docker build`
242
-
243
- ## Baseline Scores
244
-
245
- The fixed-level benchmark was expanded from the earlier 15-question set to a 30-question set with a larger seeded graph, so older benchmark artifacts should be treated as legacy and regenerated on the new dataset before using them as reference scores.
246
-
247
- After you supply an OpenAI API key, the current baseline scores for the expanded benchmark will be written to:
248
-
249
- - `artifacts/baselines/openai_fixed_levels_latest.json`
250
- - `artifacts/baselines/openai_fixed_levels_dashboard.html`
251
-
252
- ## Notes On `pyproject.toml`
253
-
254
- The packaging file is structurally correct for a `src/` layout and editable installs. The main gaps were deployment/runtime related rather than build-breaking:
255
-
256
- - `openenv` is now version-bounded explicitly.
257
- - `fastapi` and `uvicorn` are included because the repo now ships a real web server.
258
- - pytest is pointed at the `tests/` directory, and the test suite also adds `src/` to `sys.path` so source-layout imports work reliably during local runs.
259
-
260
- ## Development Notes
261
-
262
- The project keeps a lightweight local compatibility shim for `openenv` so the source tree remains importable even before dependencies are installed. In a normal install or Docker build, the real `openenv` package from PyPI is still used.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scratch/osint/artifacts/leaderboard.json DELETED
@@ -1,453 +0,0 @@
1
- [
2
- {
3
- "config": {
4
- "max_agents": 3,
5
- "max_breadth": 2,
6
- "max_depth": 2,
7
- "max_steps": 18,
8
- "max_width": 2,
9
- "seed": 7,
10
- "seeded_questions": 1,
11
- "swarm_enabled": true
12
- },
13
- "created_at": "2026-04-01T12:03:13+00:00",
14
- "episodes": 2,
15
- "metrics": {
16
- "avg_compactness_reward": 0.0,
17
- "avg_connectivity_gain_reward": 0.1,
18
- "avg_connectivity_reward": 0.3,
19
- "avg_diversity_reward": 0.08,
20
- "avg_entity_informativeness_reward": 0.024705877237863647,
21
- "avg_format_reward": 0.15,
22
- "avg_graph_f1": 1.0,
23
- "avg_knowledge_carrier_reward": 0.5,
24
- "avg_knowledge_indexing_reward": 0.15,
25
- "avg_relation_informativeness_reward": 0.03137141693971891,
26
- "avg_reward": 3.534162700533434,
27
- "avg_soft_shaping_reward": 0.15,
28
- "avg_spawn_count": 4.0,
29
- "avg_spawn_critical_steps": 6.0,
30
- "avg_steps_to_solution": 9.0,
31
- "deanonymization_accuracy": 1.0,
32
- "leaderboard_score": 0.8618382743087459,
33
- "retrieval_signal": 0.7275,
34
- "spawn_completion_rate": 1.0,
35
- "spawn_signal": 0.6666666666666666,
36
- "structural_signal": 0.6082154588355165,
37
- "task_success_rate": 1.0,
38
- "tool_efficiency": 0.25
39
- },
40
- "run_id": "run_0001",
41
- "run_name": "swarm_seed_smoke"
42
- },
43
- {
44
- "config": {
45
- "max_agents": 3,
46
- "max_breadth": 2,
47
- "max_depth": 2,
48
- "max_steps": 18,
49
- "max_width": 2,
50
- "seed": 7,
51
- "seeded_questions": 1,
52
- "swarm_enabled": true
53
- },
54
- "created_at": "2026-04-01T12:16:28+00:00",
55
- "episodes": 2,
56
- "metrics": {
57
- "avg_compactness_reward": 0.0,
58
- "avg_connectivity_gain_reward": 0.1,
59
- "avg_connectivity_reward": 0.3,
60
- "avg_diversity_reward": 0.08,
61
- "avg_entity_informativeness_reward": 0.024705877237863647,
62
- "avg_format_reward": 0.15,
63
- "avg_graph_f1": 1.0,
64
- "avg_knowledge_carrier_reward": 0.5,
65
- "avg_knowledge_indexing_reward": 0.15,
66
- "avg_relation_informativeness_reward": 0.03137141693971891,
67
- "avg_reward": 3.534162700533434,
68
- "avg_soft_shaping_reward": 0.15,
69
- "avg_spawn_count": 4.0,
70
- "avg_spawn_critical_steps": 6.0,
71
- "avg_steps_to_solution": 9.0,
72
- "deanonymization_accuracy": 1.0,
73
- "leaderboard_score": 0.8618382743087459,
74
- "retrieval_signal": 0.7275,
75
- "spawn_completion_rate": 1.0,
76
- "spawn_signal": 0.6666666666666666,
77
- "structural_signal": 0.6082154588355165,
78
- "task_success_rate": 1.0,
79
- "tool_efficiency": 0.25
80
- },
81
- "run_id": "run_0002",
82
- "run_name": "swarm_seed_smoke"
83
- },
84
- {
85
- "config": {
86
- "max_agents": 3,
87
- "max_breadth": 2,
88
- "max_depth": 2,
89
- "max_steps": 18,
90
- "max_width": 2,
91
- "seed": 7,
92
- "seeded_questions": 0,
93
- "swarm_enabled": true
94
- },
95
- "created_at": "2026-04-01T12:25:15+00:00",
96
- "episodes": 20,
97
- "metrics": {
98
- "avg_compactness_reward": 0.0,
99
- "avg_connectivity_gain_reward": 0.10000000000000002,
100
- "avg_connectivity_reward": 0.23999999999999994,
101
- "avg_diversity_reward": 0.08000000000000002,
102
- "avg_entity_informativeness_reward": -0.00983642442912193,
103
- "avg_format_reward": 0.14999999999999997,
104
- "avg_graph_f1": 1.0,
105
- "avg_knowledge_carrier_reward": 0.5,
106
- "avg_knowledge_indexing_reward": 0.1125,
107
- "avg_relation_informativeness_reward": 0.007185245326892638,
108
- "avg_reward": 3.351267560586956,
109
- "avg_soft_shaping_reward": 0.14999999999999997,
110
- "avg_spawn_count": 4.0,
111
- "avg_spawn_critical_steps": 6.0,
112
- "avg_steps_to_solution": 9.0,
113
- "deanonymization_accuracy": 1.0,
114
- "leaderboard_score": 0.8573187614039594,
115
- "retrieval_signal": 0.7143750000000001,
116
- "spawn_completion_rate": 1.0,
117
- "spawn_signal": 0.6666666666666666,
118
- "structural_signal": 0.5814697641795541,
119
- "task_success_rate": 1.0,
120
- "tool_efficiency": 0.25
121
- },
122
- "run_id": "run_0003",
123
- "run_name": "baseline_swarm"
124
- },
125
- {
126
- "config": {
127
- "max_agents": 3,
128
- "max_breadth": 2,
129
- "max_depth": 2,
130
- "max_steps": 18,
131
- "max_width": 2,
132
- "seed": 7,
133
- "seeded_questions": 1,
134
- "swarm_enabled": true
135
- },
136
- "created_at": "2026-04-01T17:27:30+00:00",
137
- "episodes": 1,
138
- "metrics": {
139
- "avg_compactness_reward": 0.0,
140
- "avg_connectivity_gain_reward": 0.1,
141
- "avg_connectivity_reward": 0.3,
142
- "avg_diversity_reward": 0.08,
143
- "avg_entity_informativeness_reward": 0.06128386989162576,
144
- "avg_format_reward": 0.15,
145
- "avg_graph_f1": 1.0,
146
- "avg_knowledge_carrier_reward": 0.5,
147
- "avg_knowledge_indexing_reward": 0.3,
148
- "avg_relation_informativeness_reward": 0.12,
149
- "avg_reward": 3.916035942914144,
150
- "avg_soft_shaping_reward": 0.15,
151
- "avg_spawn_count": 4.0,
152
- "avg_spawn_critical_steps": 6.0,
153
- "avg_steps_to_solution": 9.0,
154
- "deanonymization_accuracy": 1.0,
155
- "leaderboard_score": 0.8718832338515622,
156
- "retrieval_signal": 0.78,
157
- "spawn_completion_rate": 1.0,
158
- "spawn_signal": 0.6666666666666666,
159
- "structural_signal": 0.6332567739783251,
160
- "task_success_rate": 1.0,
161
- "tool_efficiency": 0.25
162
- },
163
- "run_id": "run_0004",
164
- "run_name": "ollama_qwen_smoke"
165
- },
166
- {
167
- "config": {
168
- "max_agents": 3,
169
- "max_breadth": 2,
170
- "max_depth": 2,
171
- "max_steps": 18,
172
- "max_width": 2,
173
- "seed": 7,
174
- "seeded_questions": 1,
175
- "swarm_enabled": true
176
- },
177
- "created_at": "2026-04-01T17:29:12+00:00",
178
- "episodes": 1,
179
- "metrics": {
180
- "avg_compactness_reward": 0.0,
181
- "avg_connectivity_gain_reward": 0.1,
182
- "avg_connectivity_reward": 0.3,
183
- "avg_diversity_reward": 0.08,
184
- "avg_entity_informativeness_reward": 0.06128386989162576,
185
- "avg_format_reward": 0.15,
186
- "avg_graph_f1": 1.0,
187
- "avg_knowledge_carrier_reward": 0.5,
188
- "avg_knowledge_indexing_reward": 0.3,
189
- "avg_relation_informativeness_reward": 0.12,
190
- "avg_reward": 4.059369276247478,
191
- "avg_soft_shaping_reward": 0.15,
192
- "avg_spawn_count": 4.0,
193
- "avg_spawn_critical_steps": 6.0,
194
- "avg_steps_to_solution": 9.0,
195
- "deanonymization_accuracy": 1.0,
196
- "leaderboard_score": 0.9020114237119466,
197
- "retrieval_signal": 0.78,
198
- "spawn_completion_rate": 1.0,
199
- "spawn_signal": 0.6666666666666666,
200
- "structural_signal": 0.6332567739783251,
201
- "task_success_rate": 1.0,
202
- "tool_efficiency": 0.5
203
- },
204
- "run_id": "run_0005",
205
- "run_name": "ollama_qwen_smoke2"
206
- },
207
- {
208
- "config": {
209
- "max_agents": 3,
210
- "max_breadth": 2,
211
- "max_depth": 2,
212
- "max_steps": 18,
213
- "max_width": 2,
214
- "seed": 7,
215
- "seeded_questions": 0,
216
- "swarm_enabled": true
217
- },
218
- "created_at": "2026-04-01T17:39:15+00:00",
219
- "episodes": 2,
220
- "metrics": {
221
- "avg_compactness_reward": 0.0,
222
- "avg_connectivity_gain_reward": 0.2,
223
- "avg_connectivity_reward": 0.0,
224
- "avg_diversity_reward": 0.0683333333333333,
225
- "avg_entity_informativeness_reward": -0.07397348480982455,
226
- "avg_format_reward": 0.15,
227
- "avg_graph_f1": 0.6666666666666667,
228
- "avg_knowledge_carrier_reward": 0.5,
229
- "avg_knowledge_indexing_reward": 0.14884615384615385,
230
- "avg_relation_informativeness_reward": -0.00860389783205907,
231
- "avg_reward": 4.351764433970379,
232
- "avg_soft_shaping_reward": 0.3,
233
- "avg_spawn_count": 4.0,
234
- "avg_spawn_critical_steps": 6.0,
235
- "avg_steps_to_solution": 9.0,
236
- "deanonymization_accuracy": 0.0,
237
- "leaderboard_score": 0.6973935600514568,
238
- "retrieval_signal": 0.7270961538461539,
239
- "spawn_completion_rate": 1.0,
240
- "spawn_signal": 0.6666666666666666,
241
- "structural_signal": 0.5137345234716233,
242
- "task_success_rate": 1.0,
243
- "tool_efficiency": 0.5
244
- },
245
- "run_id": "run_0006",
246
- "run_name": "high_timeout_shared_ctx"
247
- },
248
- {
249
- "config": {
250
- "max_agents": 3,
251
- "max_breadth": 2,
252
- "max_depth": 2,
253
- "max_steps": 18,
254
- "max_width": 2,
255
- "seed": 7,
256
- "seeded_questions": 0,
257
- "swarm_enabled": true
258
- },
259
- "created_at": "2026-04-01T18:57:40+00:00",
260
- "episodes": 3,
261
- "metrics": {
262
- "avg_compactness_reward": 0.0,
263
- "avg_connectivity_gain_reward": 0.13333333333333333,
264
- "avg_connectivity_reward": 0.09999999999999999,
265
- "avg_diversity_reward": 0.056666666666666664,
266
- "avg_entity_informativeness_reward": -0.020478979694240708,
267
- "avg_format_reward": 0.15,
268
- "avg_graph_f1": 0.8148148148148149,
269
- "avg_knowledge_carrier_reward": 0.5,
270
- "avg_knowledge_indexing_reward": 0.27,
271
- "avg_relation_informativeness_reward": 0.07174291752145656,
272
- "avg_reward": 4.0269419367756605,
273
- "avg_soft_shaping_reward": 0.19999999999999998,
274
- "avg_spawn_count": 4.0,
275
- "avg_spawn_critical_steps": 6.0,
276
- "avg_steps_to_solution": 9.0,
277
- "deanonymization_accuracy": 0.0,
278
- "leaderboard_score": 0.7366215569569294,
279
- "retrieval_signal": 0.7695000000000001,
280
- "spawn_completion_rate": 1.0,
281
- "spawn_signal": 0.6666666666666666,
282
- "structural_signal": 0.5570861208987765,
283
- "task_success_rate": 1.0,
284
- "tool_efficiency": 0.5
285
- },
286
- "run_id": "run_0007",
287
- "run_name": "episode_selector_check"
288
- },
289
- {
290
- "config": {
291
- "max_agents": 3,
292
- "max_breadth": 2,
293
- "max_depth": 2,
294
- "max_steps": 18,
295
- "max_width": 2,
296
- "seed": 7,
297
- "seeded_questions": 15,
298
- "swarm_enabled": true
299
- },
300
- "created_at": "2026-04-01T19:11:44+00:00",
301
- "episodes": 3,
302
- "metrics": {
303
- "avg_compactness_reward": 0.0,
304
- "avg_connectivity_gain_reward": 0.10000000000000002,
305
- "avg_connectivity_reward": 0.3,
306
- "avg_diversity_reward": 0.08,
307
- "avg_entity_informativeness_reward": -0.02722031691758704,
308
- "avg_format_reward": 0.15,
309
- "avg_graph_f1": 1.0,
310
- "avg_knowledge_carrier_reward": 0.5,
311
- "avg_knowledge_indexing_reward": 0.0,
312
- "avg_relation_informativeness_reward": -0.00011920119799207429,
313
- "avg_reward": 3.444079221573606,
314
- "avg_soft_shaping_reward": 0.15,
315
- "avg_spawn_count": 4.0,
316
- "avg_spawn_critical_steps": 6.0,
317
- "avg_steps_to_solution": 9.0,
318
- "deanonymization_accuracy": 1.0,
319
- "leaderboard_score": 0.8828572592896698,
320
- "retrieval_signal": 0.675,
321
- "spawn_completion_rate": 1.0,
322
- "spawn_signal": 0.6666666666666666,
323
- "structural_signal": 0.5915320963768841,
324
- "task_success_rate": 1.0,
325
- "tool_efficiency": 0.5
326
- },
327
- "run_id": "run_0008",
328
- "run_name": "qwen_rerun"
329
- },
330
- {
331
- "config": {
332
- "max_agents": 3,
333
- "max_breadth": 2,
334
- "max_depth": 2,
335
- "max_steps": 18,
336
- "max_width": 2,
337
- "seed": 7,
338
- "seeded_questions": 15,
339
- "swarm_enabled": true
340
- },
341
- "created_at": "2026-04-01T19:19:34+00:00",
342
- "episodes": 3,
343
- "metrics": {
344
- "avg_compactness_reward": 0.0,
345
- "avg_connectivity_gain_reward": 0.10000000000000002,
346
- "avg_connectivity_reward": 0.3,
347
- "avg_diversity_reward": 0.08,
348
- "avg_entity_informativeness_reward": -0.024861029515896544,
349
- "avg_format_reward": 0.15,
350
- "avg_graph_f1": 1.0,
351
- "avg_knowledge_carrier_reward": 0.5,
352
- "avg_knowledge_indexing_reward": 0.0,
353
- "avg_relation_informativeness_reward": -0.0024320085090966614,
354
- "avg_reward": 3.4441257016641917,
355
- "avg_soft_shaping_reward": 0.15,
356
- "avg_spawn_count": 4.0,
357
- "avg_spawn_critical_steps": 6.0,
358
- "avg_steps_to_solution": 9.0,
359
- "deanonymization_accuracy": 1.0,
360
- "leaderboard_score": 0.8828581656226586,
361
- "retrieval_signal": 0.675,
362
- "spawn_completion_rate": 1.0,
363
- "spawn_signal": 0.6666666666666666,
364
- "structural_signal": 0.5915413923950014,
365
- "task_success_rate": 1.0,
366
- "tool_efficiency": 0.5
367
- },
368
- "run_id": "run_0009",
369
- "run_name": "qwen_episode_fix"
370
- },
371
- {
372
- "config": {
373
- "max_agents": 3,
374
- "max_breadth": 2,
375
- "max_depth": 2,
376
- "max_steps": 18,
377
- "max_width": 2,
378
- "seed": 7,
379
- "seeded_questions": 15,
380
- "swarm_enabled": true
381
- },
382
- "created_at": "2026-04-01T19:24:37+00:00",
383
- "episodes": 3,
384
- "metrics": {
385
- "avg_compactness_reward": 0.0,
386
- "avg_connectivity_gain_reward": 0.10000000000000002,
387
- "avg_connectivity_reward": 0.3,
388
- "avg_diversity_reward": 0.08,
389
- "avg_entity_informativeness_reward": -0.02722031691758704,
390
- "avg_format_reward": 0.15,
391
- "avg_graph_f1": 1.0,
392
- "avg_knowledge_carrier_reward": 0.5,
393
- "avg_knowledge_indexing_reward": 0.0,
394
- "avg_relation_informativeness_reward": -0.0030604289114462002,
395
- "avg_reward": 3.4411379938601514,
396
- "avg_soft_shaping_reward": 0.15,
397
- "avg_spawn_count": 4.0,
398
- "avg_spawn_critical_steps": 6.0,
399
- "avg_steps_to_solution": 9.0,
400
- "deanonymization_accuracy": 1.0,
401
- "leaderboard_score": 0.8827999009847504,
402
- "retrieval_signal": 0.675,
403
- "spawn_completion_rate": 1.0,
404
- "spawn_signal": 0.6666666666666666,
405
- "structural_signal": 0.5909438508341933,
406
- "task_success_rate": 1.0,
407
- "tool_efficiency": 0.5
408
- },
409
- "run_id": "run_0010",
410
- "run_name": "qwen_rerun_graph_fix"
411
- },
412
- {
413
- "config": {
414
- "max_agents": 3,
415
- "max_breadth": 2,
416
- "max_depth": 2,
417
- "max_steps": 18,
418
- "max_width": 2,
419
- "seed": 7,
420
- "seeded_questions": 15,
421
- "swarm_enabled": true
422
- },
423
- "created_at": "2026-04-01T19:31:54+00:00",
424
- "episodes": 15,
425
- "metrics": {
426
- "avg_compactness_reward": 0.0,
427
- "avg_connectivity_gain_reward": 0.16666666666666666,
428
- "avg_connectivity_reward": 0.16999999999999998,
429
- "avg_diversity_reward": 0.1157777777777778,
430
- "avg_entity_informativeness_reward": -0.0181244777358718,
431
- "avg_format_reward": 0.14999999999999997,
432
- "avg_graph_f1": 0.8492063492063492,
433
- "avg_knowledge_carrier_reward": 0.5,
434
- "avg_knowledge_indexing_reward": 0.012000000000000002,
435
- "avg_relation_informativeness_reward": 0.05935837081627929,
436
- "avg_reward": 4.201760569277529,
437
- "avg_soft_shaping_reward": 0.24999999999999994,
438
- "avg_spawn_count": 4.0,
439
- "avg_spawn_critical_steps": 6.0,
440
- "avg_steps_to_solution": 9.0,
441
- "deanonymization_accuracy": 1.0,
442
- "leaderboard_score": 0.8534887252258901,
443
- "retrieval_signal": 0.6792,
444
- "spawn_completion_rate": 1.0,
445
- "spawn_signal": 0.6666666666666666,
446
- "structural_signal": 0.5847801119494148,
447
- "task_success_rate": 1.0,
448
- "tool_efficiency": 0.5
449
- },
450
- "run_id": "run_0011",
451
- "run_name": "qwen_rerun_graph_fix"
452
- }
453
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scratch/osint/artifacts/osint_dashboard.html DELETED
The diff for this file is too large to render. See raw diff
 
scratch/osint/config/seed_example.json DELETED
@@ -1,52 +0,0 @@
1
- {
2
- "seeding": {
3
- "seeded_nodes": [
4
- {
5
- "node_id": "alias_seed_001",
6
- "node_type": "alias",
7
- "attrs": {
8
- "handle": "@shadow_seed"
9
- }
10
- },
11
- {
12
- "node_id": "user_seed_001",
13
- "node_type": "user",
14
- "attrs": {
15
- "name": "Seed User",
16
- "org": "Helios Labs",
17
- "location": "Pune"
18
- }
19
- }
20
- ],
21
- "_note": "Use with --seed-file. LLM provider and API keys are configured in config/shared_config.json or CLI flags.",
22
- "seeded_edges": [
23
- {
24
- "src": "alias_seed_001",
25
- "rel": "alias_of",
26
- "dst": "user_seed_001",
27
- "confidence": 1.0
28
- }
29
- ],
30
- "seeded_questions": [
31
- {
32
- "task_type": "identity_resolution",
33
- "question": "Which canonical user owns alias alias_seed_001?",
34
- "answer": "user_seed_001",
35
- "supporting_edges": [
36
- {
37
- "src": "alias_seed_001",
38
- "rel": "alias_of",
39
- "dst": "user_seed_001"
40
- }
41
- ],
42
- "metadata": {
43
- "source": "manual_seed"
44
- }
45
- }
46
- ],
47
- "llm_generate_remaining_graph": true,
48
- "llm_generate_remaining_tasks": true,
49
- "llm_generated_edge_budget": 6,
50
- "llm_generated_task_budget": 8
51
- }
52
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scratch/osint/config/seed_ollama_smoke.json DELETED
@@ -1,51 +0,0 @@
1
- {
2
- "seeding": {
3
- "seeded_nodes": [
4
- {
5
- "node_id": "alias_smoke_001",
6
- "node_type": "alias",
7
- "attrs": {
8
- "handle": "@smoke_alias"
9
- }
10
- },
11
- {
12
- "node_id": "user_smoke_001",
13
- "node_type": "user",
14
- "attrs": {
15
- "name": "Smoke User",
16
- "org": "Apex Dynamics",
17
- "location": "Bengaluru"
18
- }
19
- }
20
- ],
21
- "seeded_edges": [
22
- {
23
- "src": "alias_smoke_001",
24
- "rel": "alias_of",
25
- "dst": "user_smoke_001",
26
- "confidence": 1.0
27
- }
28
- ],
29
- "seeded_questions": [
30
- {
31
- "task_type": "identity_resolution",
32
- "question": "Which canonical user owns alias alias_smoke_001?",
33
- "answer": "user_smoke_001",
34
- "supporting_edges": [
35
- {
36
- "src": "alias_smoke_001",
37
- "rel": "alias_of",
38
- "dst": "user_smoke_001"
39
- }
40
- ],
41
- "metadata": {
42
- "source": "ollama_smoke"
43
- }
44
- }
45
- ],
46
- "llm_generate_remaining_graph": false,
47
- "llm_generate_remaining_tasks": false,
48
- "llm_generated_edge_budget": 0,
49
- "llm_generated_task_budget": 0
50
- }
51
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scratch/osint/config/shared_config.json DELETED
@@ -1,55 +0,0 @@
1
- {
2
- "environment": {
3
- "n_users": 40,
4
- "alias_density": 0.35,
5
- "noise_level": 0.15,
6
- "red_herring_rate": 0.1,
7
- "max_steps": 18,
8
- "seed": 7
9
- },
10
- "swarm": {
11
- "enabled": true,
12
- "max_agents": 3,
13
- "max_breadth": 2,
14
- "max_width": 2,
15
- "max_depth": 2,
16
- "planner_rounds": 2,
17
- "tools_per_agent": 1
18
- },
19
- "spawn_reward": {
20
- "lambda_parallel": 0.15,
21
- "lambda_finish": 0.2,
22
- "anneal": 1.0,
23
- "max_parallel_hint": 3
24
- },
25
- "seeding": {
26
- "seeded_nodes": [],
27
- "seeded_edges": [],
28
- "seeded_questions": [],
29
- "llm_generate_remaining_graph": true,
30
- "llm_generate_remaining_tasks": true,
31
- "llm_generated_edge_budget": 6,
32
- "llm_generated_task_budget": 8,
33
- "llm_generation_parallel": true,
34
- "llm_generation_workers": 3,
35
- "llm_generation_retries": 2,
36
- "allow_template_fallback_on_llm_failure": false
37
- },
38
- "llm": {
39
- "provider": "ollama",
40
- "model": "qwen3:2b",
41
- "temperature": 0.1,
42
- "max_tokens": 256,
43
- "timeout_seconds": 240,
44
- "ollama_base_url": "http://127.0.0.1:11434",
45
- "openai_base_url": "https://api.openai.com/v1",
46
- "openai_api_key_env": "OPENAI_API_KEY",
47
- "openai_api_key": ""
48
- },
49
- "runtime": {
50
- "default_episodes": 20,
51
- "leaderboard_path": "artifacts/leaderboard.json",
52
- "dashboard_path": "artifacts/osint_dashboard.html",
53
- "sweep_dashboard_dir": "artifacts/sweep_dashboards"
54
- }
55
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scratch/osint/datasets/fixed_levels/README.md DELETED
@@ -1,64 +0,0 @@
1
- # Fixed Levels Submission Dataset
2
-
3
- This folder contains a fixed three-level OSINT benchmark set built on one shared base graph.
4
-
5
- ## Files
6
-
7
- - `seed_fixed_levels.json`: master fixed seed with an expanded canonical graph and 30 fixed questions.
8
- - `fixed_graph_questions.json`: extracted fixed dataset snapshot for submission packaging.
9
- - `shared_config_fixed_levels.json`: run config used for generation and evaluation.
10
- - `complete_dataset_qwen_generated.json`: full dataset after Qwen (`qwen3:2b` via Ollama) expands the graph.
11
- - `qwen_swarm_eval_fixed_levels.json`: legacy Qwen swarm evaluation summary from the older smaller version of the set.
12
- - `qwen_swarm_benchmark_fixed_levels.json`: legacy benchmark output from the older smaller version of the set.
13
- - `leaderboard_fixed_levels.json`: leaderboard file for this dataset.
14
- - `dashboard_fixed_levels.html`: interactive dashboard generated from the benchmark run.
15
-
16
- ## Difficulty Design
17
-
18
- - Easy: 10 questions. These now use the older hard-style multi-hop traces as the new floor.
19
- - Mid: 10 questions. Each question spans roughly 15-20 supporting nodes.
20
- - High: 10 questions. Each question spans roughly 50 supporting nodes.
21
-
22
- All 30 questions are fixed and share the same larger seeded graph.
23
-
24
- ## Regenerate Artifacts
25
-
26
- ```bash
27
- source ~/arl/bin/activate
28
- cd /home/ritish/test1
29
- PYTHONPATH=src python scripts/build_fixed_levels_dataset.py \
30
- --seed-file datasets/fixed_levels/seed_fixed_levels.json \
31
- --shared-config datasets/fixed_levels/shared_config_fixed_levels.json \
32
- --output-dir datasets/fixed_levels
33
- ```
34
-
35
- ## Evaluate Qwen Swarm
36
-
37
- ```bash
38
- source ~/arl/bin/activate
39
- cd /home/ritish/test1
40
- PYTHONPATH=src osint-env eval \
41
- --config datasets/fixed_levels/shared_config_fixed_levels.json \
42
- --seed-file datasets/fixed_levels/seed_fixed_levels.json \
43
- --agent-mode swarm \
44
- --llm-provider ollama \
45
- --llm-model qwen3:2b \
46
- --episodes 15
47
- ```
48
-
49
- ## Benchmark + Dashboard
50
-
51
- ```bash
52
- source ~/arl/bin/activate
53
- cd /home/ritish/test1
54
- PYTHONPATH=src osint-env benchmark \
55
- --config datasets/fixed_levels/shared_config_fixed_levels.json \
56
- --seed-file datasets/fixed_levels/seed_fixed_levels.json \
57
- --agent-mode swarm \
58
- --llm-provider ollama \
59
- --llm-model qwen3:2b \
60
- --episodes 15 \
61
- --name fixed_levels_qwen_swarm \
62
- --leaderboard datasets/fixed_levels/leaderboard_fixed_levels.json \
63
- --dashboard datasets/fixed_levels/dashboard_fixed_levels.html
64
- ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scratch/osint/datasets/fixed_levels/complete_dataset_qwen_generated.json DELETED
The diff for this file is too large to render. See raw diff
 
scratch/osint/datasets/fixed_levels/fixed_graph_questions.json DELETED
The diff for this file is too large to render. See raw diff
 
scratch/osint/datasets/fixed_levels/leaderboard_fixed_levels.json DELETED
@@ -1,1229 +0,0 @@
1
- [
2
- {
3
- "config": {
4
- "max_agents": 3,
5
- "max_breadth": 2,
6
- "max_depth": 2,
7
- "max_steps": 20,
8
- "max_width": 2,
9
- "seed": 2026,
10
- "seeded_questions": 15,
11
- "swarm_enabled": true
12
- },
13
- "created_at": "2026-04-01T18:48:39+00:00",
14
- "episodes": 15,
15
- "metrics": {
16
- "avg_compactness_reward": 0.0,
17
- "avg_connectivity_gain_reward": 0.16666666666666666,
18
- "avg_connectivity_reward": 0.16999999999999998,
19
- "avg_diversity_reward": 0.1157777777777778,
20
- "avg_entity_informativeness_reward": -0.08858065677817137,
21
- "avg_format_reward": 0.14999999999999997,
22
- "avg_graph_f1": 0.8492063492063492,
23
- "avg_knowledge_carrier_reward": 0.5,
24
- "avg_knowledge_indexing_reward": 0.052000000000000005,
25
- "avg_relation_informativeness_reward": 0.07135858524047924,
26
- "avg_reward": 4.197526826881651,
27
- "avg_soft_shaping_reward": 0.24999999999999994,
28
- "avg_spawn_count": 4.0,
29
- "avg_spawn_critical_steps": 6.0,
30
- "avg_steps_to_solution": 9.0,
31
- "deanonymization_accuracy": 1.0,
32
- "leaderboard_score": 0.8543934355282199,
33
- "retrieval_signal": 0.6932,
34
- "spawn_completion_rate": 1.0,
35
- "spawn_signal": 0.6666666666666666,
36
- "structural_signal": 0.5730889190257948,
37
- "task_success_rate": 1.0,
38
- "tool_efficiency": 0.5
39
- },
40
- "run_id": "run_0001",
41
- "run_name": "fixed_levels_qwen_swarm"
42
- },
43
- {
44
- "config": {
45
- "max_agents": 3,
46
- "max_breadth": 2,
47
- "max_depth": 2,
48
- "max_steps": 24,
49
- "max_width": 2,
50
- "seed": 2026,
51
- "seeded_questions": 30,
52
- "swarm_enabled": true
53
- },
54
- "created_at": "2026-04-02T09:16:05+00:00",
55
- "episodes": 30,
56
- "metrics": {
57
- "avg_compactness_reward": 0.0,
58
- "avg_connectivity_gain_reward": 0.2000000000000001,
59
- "avg_connectivity_reward": 0.12999999999999998,
60
- "avg_diversity_reward": 0.12433333333333325,
61
- "avg_entity_informativeness_reward": 0.000700571890338102,
62
- "avg_format_reward": 0.15,
63
- "avg_graph_f1": 0.2916528337385394,
64
- "avg_knowledge_carrier_reward": 0.5,
65
- "avg_knowledge_indexing_reward": 0.05070078042510192,
66
- "avg_relation_informativeness_reward": 0.07853375358885142,
67
- "avg_reward": 4.377456514967488,
68
- "avg_soft_shaping_reward": 0.3,
69
- "avg_spawn_count": 4.0,
70
- "avg_spawn_critical_steps": 6.0,
71
- "avg_steps_to_solution": 9.0,
72
- "deanonymization_accuracy": 0.0,
73
- "leaderboard_score": 0.6241912131110795,
74
- "retrieval_signal": 0.6927452731487858,
75
- "spawn_completion_rate": 1.0,
76
- "spawn_signal": 0.6666666666666666,
77
- "structural_signal": 0.5869968650958378,
78
- "task_success_rate": 1.0,
79
- "tool_efficiency": 0.5
80
- },
81
- "run_id": "run_0002",
82
- "run_name": "fixed_levels_qwen_swarm"
83
- },
84
- {
85
- "config": {
86
- "max_agents": 3,
87
- "max_breadth": 2,
88
- "max_depth": 2,
89
- "max_steps": 24,
90
- "max_width": 2,
91
- "seed": 2026,
92
- "seeded_questions": 30,
93
- "swarm_enabled": true
94
- },
95
- "created_at": "2026-04-03T13:22:03+00:00",
96
- "episodes": 3,
97
- "metrics": {
98
- "avg_compactness_reward": 0.0,
99
- "avg_connectivity_gain_reward": 0.20000000000000004,
100
- "avg_connectivity_reward": -0.06666666666666667,
101
- "avg_diversity_reward": 0.13444444444444445,
102
- "avg_entity_informativeness_reward": -0.01010882862863417,
103
- "avg_format_reward": 0.15,
104
- "avg_graph_f1": 0.5793650793650794,
105
- "avg_knowledge_carrier_reward": 0.5,
106
- "avg_knowledge_indexing_reward": 0.10372960372960373,
107
- "avg_relation_informativeness_reward": 0.07108687894082726,
108
- "avg_reward": 4.419313576918165,
109
- "avg_soft_shaping_reward": 0.3,
110
- "avg_spawn_count": 4.0,
111
- "avg_spawn_critical_steps": 6.0,
112
- "avg_steps_to_solution": 9.0,
113
- "deanonymization_accuracy": 0.0,
114
- "leaderboard_score": 0.6797400780463063,
115
- "retrieval_signal": 0.7113053613053614,
116
- "spawn_completion_rate": 1.0,
117
- "spawn_signal": 0.6666666666666666,
118
- "structural_signal": 0.5356956100624386,
119
- "task_success_rate": 1.0,
120
- "tool_efficiency": 0.5
121
- },
122
- "run_id": "run_0003",
123
- "run_name": "fixed_levels_qwen_swarm"
124
- },
125
- {
126
- "config": {
127
- "max_agents": 3,
128
- "max_breadth": 2,
129
- "max_depth": 2,
130
- "max_steps": 24,
131
- "max_width": 2,
132
- "seed": 2026,
133
- "seeded_questions": 30,
134
- "swarm_enabled": true
135
- },
136
- "created_at": "2026-04-06T18:29:39+00:00",
137
- "episodes": 30,
138
- "metrics": {
139
- "avg_compactness_reward": 0.0,
140
- "avg_connectivity_gain_reward": 0.2000000000000001,
141
- "avg_connectivity_reward": 0.12999999999999998,
142
- "avg_diversity_reward": 0.12433333333333325,
143
- "avg_entity_informativeness_reward": -0.02515191749984708,
144
- "avg_format_reward": 0.15,
145
- "avg_graph_f1": 0.2916528337385394,
146
- "avg_knowledge_carrier_reward": 0.5,
147
- "avg_knowledge_indexing_reward": 0.11539120363588044,
148
- "avg_relation_informativeness_reward": 0.0769903534735767,
149
- "avg_reward": 4.460667345528021,
150
- "avg_soft_shaping_reward": 0.3,
151
- "avg_spawn_count": 4.0,
152
- "avg_spawn_critical_steps": 6.0,
153
- "avg_steps_to_solution": 9.0,
154
- "deanonymization_accuracy": 0.0,
155
- "leaderboard_score": 0.6269168609961595,
156
- "retrieval_signal": 0.7153869212725582,
157
- "spawn_completion_rate": 1.0,
158
- "spawn_signal": 0.6666666666666666,
159
- "structural_signal": 0.5815176871947458,
160
- "task_success_rate": 1.0,
161
- "tool_efficiency": 0.5
162
- },
163
- "run_id": "run_0004",
164
- "run_name": "fixed_levels_qwen_swarm"
165
- },
166
- {
167
- "config": {
168
- "max_agents": 3,
169
- "max_breadth": 2,
170
- "max_depth": 2,
171
- "max_steps": 24,
172
- "max_width": 2,
173
- "seed": 2026,
174
- "seeded_questions": 30,
175
- "swarm_enabled": true
176
- },
177
- "created_at": "2026-04-06T18:33:06+00:00",
178
- "episodes": 2,
179
- "metrics": {
180
- "avg_compactness_reward": 0.0,
181
- "avg_connectivity_gain_reward": 0.2,
182
- "avg_connectivity_reward": -0.15,
183
- "avg_diversity_reward": 0.13833333333333334,
184
- "avg_entity_informativeness_reward": -0.026628229842114173,
185
- "avg_format_reward": 0.15,
186
- "avg_graph_f1": 0.6190476190476191,
187
- "avg_knowledge_carrier_reward": 0.5,
188
- "avg_knowledge_indexing_reward": 0.10681818181818181,
189
- "avg_relation_informativeness_reward": 0.048120982127120335,
190
- "avg_reward": 4.334953339016039,
191
- "avg_soft_shaping_reward": 0.3,
192
- "avg_spawn_count": 4.0,
193
- "avg_spawn_critical_steps": 6.0,
194
- "avg_steps_to_solution": 9.0,
195
- "deanonymization_accuracy": 0.0,
196
- "leaderboard_score": 0.685242999396977,
197
- "retrieval_signal": 0.7123863636363637,
198
- "spawn_completion_rate": 1.0,
199
- "spawn_signal": 0.6666666666666666,
200
- "structural_signal": 0.5075485504570012,
201
- "task_success_rate": 1.0,
202
- "tool_efficiency": 0.5
203
- },
204
- "run_id": "run_0005",
205
- "run_name": "fixed_levels_qwen_swarm"
206
- },
207
- {
208
- "config": {
209
- "max_agents": 1,
210
- "max_breadth": 2,
211
- "max_depth": 2,
212
- "max_steps": 24,
213
- "max_width": 2,
214
- "seed": 2026,
215
- "seeded_questions": 30,
216
- "swarm_enabled": true
217
- },
218
- "created_at": "2026-04-06T18:54:52+00:00",
219
- "episodes": 1,
220
- "metrics": {
221
- "avg_compactness_reward": 0.0,
222
- "avg_connectivity_gain_reward": 0.1,
223
- "avg_connectivity_reward": -0.3,
224
- "avg_diversity_reward": 0.08,
225
- "avg_entity_informativeness_reward": -0.02450859227728558,
226
- "avg_format_reward": 0.15,
227
- "avg_graph_f1": 0.33333333333333337,
228
- "avg_knowledge_carrier_reward": 0.5,
229
- "avg_knowledge_indexing_reward": 0.08181818181818182,
230
- "avg_relation_informativeness_reward": 0.04353540016904645,
231
- "avg_reward": 3.037246438342494,
232
- "avg_soft_shaping_reward": 0.15,
233
- "avg_spawn_count": 2.0,
234
- "avg_spawn_critical_steps": 6.0,
235
- "avg_steps_to_solution": 5.0,
236
- "deanonymization_accuracy": 0.0,
237
- "leaderboard_score": 0.6201263424948862,
238
- "retrieval_signal": 0.7036363636363637,
239
- "spawn_completion_rate": 1.0,
240
- "spawn_signal": 0.6666666666666666,
241
- "structural_signal": 0.45080536157835216,
242
- "task_success_rate": 1.0,
243
- "tool_efficiency": 0.5
244
- },
245
- "run_id": "run_0006",
246
- "run_name": "fixed_levels_qwen_swarm"
247
- },
248
- {
249
- "config": {
250
- "max_agents": 1,
251
- "max_breadth": 2,
252
- "max_depth": 2,
253
- "max_steps": 24,
254
- "max_width": 2,
255
- "seed": 2026,
256
- "seeded_questions": 30,
257
- "swarm_enabled": true
258
- },
259
- "created_at": "2026-04-06T19:22:57+00:00",
260
- "episodes": 1,
261
- "metrics": {
262
- "avg_compactness_reward": 0.0,
263
- "avg_connectivity_gain_reward": 0.1,
264
- "avg_connectivity_reward": -0.3,
265
- "avg_diversity_reward": 0.08,
266
- "avg_entity_informativeness_reward": -0.005263146336646693,
267
- "avg_format_reward": 0.15,
268
- "avg_graph_f1": 0.33333333333333337,
269
- "avg_knowledge_carrier_reward": 0.5,
270
- "avg_knowledge_indexing_reward": 0.08181818181818182,
271
- "avg_relation_informativeness_reward": 0.044276243254877785,
272
- "avg_reward": 3.057232727368964,
273
- "avg_soft_shaping_reward": 0.15,
274
- "avg_spawn_count": 2.0,
275
- "avg_spawn_critical_steps": 6.0,
276
- "avg_steps_to_solution": 5.0,
277
- "deanonymization_accuracy": 0.0,
278
- "leaderboard_score": 0.6205293479318178,
279
- "retrieval_signal": 0.7036363636363637,
280
- "spawn_completion_rate": 1.0,
281
- "spawn_signal": 0.6666666666666666,
282
- "structural_signal": 0.4548026193836462,
283
- "task_success_rate": 1.0,
284
- "tool_efficiency": 0.5
285
- },
286
- "run_id": "run_0007",
287
- "run_name": "fixed_levels_qwen_swarm"
288
- },
289
- {
290
- "config": {
291
- "llm_model": "qwen3:1.7b",
292
- "llm_provider": "ollama",
293
- "max_agents": 1,
294
- "max_breadth": 2,
295
- "max_depth": 2,
296
- "max_steps": 24,
297
- "max_width": 2,
298
- "seed": 2026,
299
- "seeded_questions": 30,
300
- "swarm_enabled": true
301
- },
302
- "created_at": "2026-04-06T19:48:33+00:00",
303
- "episodes": 3,
304
- "metrics": {
305
- "avg_compactness_reward": 0.0,
306
- "avg_connectivity_gain_reward": 0.10000000000000002,
307
- "avg_connectivity_reward": -0.09999999999999999,
308
- "avg_diversity_reward": 0.08,
309
- "avg_entity_informativeness_reward": -0.028683816517602444,
310
- "avg_format_reward": 0.15,
311
- "avg_graph_f1": 0.15537340619307835,
312
- "avg_knowledge_carrier_reward": 0.5,
313
- "avg_knowledge_indexing_reward": 0.07932190760059611,
314
- "avg_relation_informativeness_reward": 0.044225025032092045,
315
- "avg_reward": 3.1324990406542437,
316
- "avg_soft_shaping_reward": 0.15,
317
- "avg_spawn_count": 2.0,
318
- "avg_spawn_critical_steps": 6.0,
319
- "avg_steps_to_solution": 5.0,
320
- "deanonymization_accuracy": 0.0,
321
- "leaderboard_score": 0.5890485416309927,
322
- "retrieval_signal": 0.7027626676602087,
323
- "spawn_completion_rate": 1.0,
324
- "spawn_signal": 0.6666666666666666,
325
- "structural_signal": 0.5001082417028979,
326
- "task_success_rate": 1.0,
327
- "tool_efficiency": 0.5
328
- },
329
- "run_id": "run_0008",
330
- "run_name": "fixed_levels_qwen_swarm"
331
- },
332
- {
333
- "config": {
334
- "llm_model": "qwen3:1.7b",
335
- "llm_provider": "ollama",
336
- "max_agents": 1,
337
- "max_breadth": 2,
338
- "max_depth": 2,
339
- "max_steps": 24,
340
- "max_width": 2,
341
- "seed": 2026,
342
- "seeded_questions": 30,
343
- "swarm_enabled": true
344
- },
345
- "created_at": "2026-04-06T19:55:08+00:00",
346
- "episodes": 1,
347
- "metrics": {
348
- "avg_compactness_reward": 0.0,
349
- "avg_connectivity_gain_reward": 0.1,
350
- "avg_connectivity_reward": -0.3,
351
- "avg_diversity_reward": 0.08,
352
- "avg_entity_informativeness_reward": -0.005263146336646693,
353
- "avg_format_reward": 0.15,
354
- "avg_graph_f1": 0.33333333333333337,
355
- "avg_knowledge_carrier_reward": 0.5,
356
- "avg_knowledge_indexing_reward": 0.08181818181818182,
357
- "avg_relation_informativeness_reward": 0.04406984773661544,
358
- "avg_reward": 3.0570263318507016,
359
- "avg_soft_shaping_reward": 0.15,
360
- "avg_spawn_count": 2.0,
361
- "avg_spawn_critical_steps": 6.0,
362
- "avg_steps_to_solution": 5.0,
363
- "deanonymization_accuracy": 0.0,
364
- "leaderboard_score": 0.6205251901591228,
365
- "retrieval_signal": 0.7036363636363637,
366
- "spawn_completion_rate": 1.0,
367
- "spawn_signal": 0.6666666666666666,
368
- "structural_signal": 0.45476134027999376,
369
- "task_success_rate": 1.0,
370
- "tool_efficiency": 0.5
371
- },
372
- "run_id": "run_0009",
373
- "run_name": "fixed_levels_qwen_swarm"
374
- },
375
- {
376
- "config": {
377
- "llm_model": "qwen3:1.7b",
378
- "llm_provider": "ollama",
379
- "max_agents": 1,
380
- "max_breadth": 2,
381
- "max_depth": 2,
382
- "max_steps": 24,
383
- "max_width": 2,
384
- "seed": 2026,
385
- "seeded_questions": 30,
386
- "swarm_enabled": true
387
- },
388
- "created_at": "2026-04-06T20:01:34+00:00",
389
- "episodes": 1,
390
- "metrics": {
391
- "avg_compactness_reward": 0.0,
392
- "avg_connectivity_gain_reward": 0.1,
393
- "avg_connectivity_reward": -0.3,
394
- "avg_diversity_reward": 0.08,
395
- "avg_entity_informativeness_reward": -0.020826953461399098,
396
- "avg_format_reward": 0.15,
397
- "avg_graph_f1": 0.33333333333333337,
398
- "avg_knowledge_carrier_reward": 0.5,
399
- "avg_knowledge_indexing_reward": 0.08181818181818182,
400
- "avg_relation_informativeness_reward": 0.04348043923536236,
401
- "avg_reward": 3.040873116224696,
402
- "avg_soft_shaping_reward": 0.15,
403
- "avg_spawn_count": 2.0,
404
- "avg_spawn_critical_steps": 6.0,
405
- "avg_steps_to_solution": 5.0,
406
- "deanonymization_accuracy": 0.0,
407
- "leaderboard_score": 0.6201995296517067,
408
- "retrieval_signal": 0.7036363636363637,
409
- "spawn_completion_rate": 1.0,
410
- "spawn_signal": 0.6666666666666666,
411
- "structural_signal": 0.45153069715479266,
412
- "task_success_rate": 1.0,
413
- "tool_efficiency": 0.5
414
- },
415
- "run_id": "run_0010",
416
- "run_name": "fixed_levels_qwen_swarm"
417
- },
418
- {
419
- "config": {
420
- "max_agents": 3,
421
- "max_breadth": 2,
422
- "max_depth": 2,
423
- "max_steps": 24,
424
- "max_width": 2,
425
- "seed": 2026,
426
- "seeded_questions": 30,
427
- "swarm_enabled": true
428
- },
429
- "created_at": "2026-04-06T20:46:11+00:00",
430
- "episodes": 1,
431
- "metrics": {
432
- "avg_compactness_reward": 0.0,
433
- "avg_connectivity_gain_reward": 0.2,
434
- "avg_connectivity_reward": -0.15,
435
- "avg_diversity_reward": 0.12666666666666665,
436
- "avg_entity_informativeness_reward": 0.019629386278697845,
437
- "avg_format_reward": 0.15,
438
- "avg_graph_f1": 0.5714285714285715,
439
- "avg_knowledge_carrier_reward": 0.5,
440
- "avg_knowledge_indexing_reward": 0.12272727272727273,
441
- "avg_relation_informativeness_reward": 0.08347928023822283,
442
- "avg_reward": 1.829702015111513,
443
- "avg_soft_shaping_reward": 0.3,
444
- "avg_spawn_count": 4.0,
445
- "avg_spawn_critical_steps": 6.0,
446
- "avg_steps_to_solution": 9.0,
447
- "deanonymization_accuracy": 0.0,
448
- "leaderboard_score": 0.6715432845394145,
449
- "retrieval_signal": 0.7179545454545455,
450
- "spawn_completion_rate": 1.0,
451
- "spawn_signal": 0.6666666666666666,
452
- "structural_signal": 0.5221217333033842,
453
- "task_success_rate": 1.0,
454
- "tool_efficiency": 0.5
455
- },
456
- "run_id": "run_0011",
457
- "run_name": "fixed_levels_qwen_swarm"
458
- },
459
- {
460
- "config": {
461
- "max_agents": 3,
462
- "max_breadth": 2,
463
- "max_depth": 2,
464
- "max_steps": 24,
465
- "max_width": 2,
466
- "seed": 2026,
467
- "seeded_questions": 30,
468
- "swarm_enabled": true
469
- },
470
- "created_at": "2026-04-06T20:49:44+00:00",
471
- "episodes": 1,
472
- "metrics": {
473
- "avg_compactness_reward": 0.0,
474
- "avg_connectivity_gain_reward": 0.2,
475
- "avg_connectivity_reward": -0.15,
476
- "avg_diversity_reward": 0.12666666666666665,
477
- "avg_entity_informativeness_reward": 0.019629386278697845,
478
- "avg_format_reward": 0.15,
479
- "avg_graph_f1": 0.5714285714285715,
480
- "avg_knowledge_carrier_reward": 0.5,
481
- "avg_knowledge_indexing_reward": 0.12272727272727273,
482
- "avg_relation_informativeness_reward": 0.08335372627068136,
483
- "avg_reward": 0.7139904233885594,
484
- "avg_soft_shaping_reward": 0.3,
485
- "avg_spawn_count": 4.0,
486
- "avg_spawn_critical_steps": 6.0,
487
- "avg_steps_to_solution": 9.0,
488
- "deanonymization_accuracy": 0.0,
489
- "leaderboard_score": 0.6641542345113342,
490
- "retrieval_signal": 0.7179545454545455,
491
- "spawn_completion_rate": 1.0,
492
- "spawn_signal": 0.6666666666666666,
493
- "structural_signal": 0.5220966225098759,
494
- "task_success_rate": 1.0,
495
- "tool_efficiency": 0.5
496
- },
497
- "run_id": "run_0012",
498
- "run_name": "fixed_levels_qwen_swarm"
499
- },
500
- {
501
- "config": {
502
- "max_agents": 3,
503
- "max_breadth": 2,
504
- "max_depth": 2,
505
- "max_steps": 24,
506
- "max_width": 2,
507
- "seed": 2026,
508
- "seeded_questions": 30,
509
- "swarm_enabled": true
510
- },
511
- "created_at": "2026-04-06T20:59:43+00:00",
512
- "episodes": 1,
513
- "metrics": {
514
- "avg_compactness_reward": 0.0,
515
- "avg_connectivity_gain_reward": 0.2,
516
- "avg_connectivity_reward": -0.15,
517
- "avg_diversity_reward": 0.12666666666666665,
518
- "avg_entity_informativeness_reward": 0.0036675120354726642,
519
- "avg_format_reward": 0.15,
520
- "avg_graph_f1": 0.5714285714285715,
521
- "avg_knowledge_carrier_reward": 0.5,
522
- "avg_knowledge_indexing_reward": 0.12272727272727273,
523
- "avg_relation_informativeness_reward": 0.08250745620050208,
524
- "avg_reward": 0.7138056720677886,
525
- "avg_soft_shaping_reward": 0.3,
526
- "avg_spawn_count": 4.0,
527
- "avg_spawn_critical_steps": 6.0,
528
- "avg_steps_to_solution": 9.0,
529
- "deanonymization_accuracy": 0.0,
530
- "leaderboard_score": 0.6638424503476543,
531
- "retrieval_signal": 0.7179545454545455,
532
- "spawn_completion_rate": 1.0,
533
- "spawn_signal": 0.6666666666666666,
534
- "structural_signal": 0.518734993647195,
535
- "task_success_rate": 1.0,
536
- "tool_efficiency": 0.5
537
- },
538
- "run_id": "run_0013",
539
- "run_name": "fixed_levels_qwen_swarm"
540
- },
541
- {
542
- "config": {
543
- "llm_model": "gpt-5.4-mini",
544
- "llm_provider": "openai",
545
- "max_agents": 3,
546
- "max_breadth": 2,
547
- "max_depth": 2,
548
- "max_steps": 24,
549
- "max_width": 2,
550
- "seed": 2026,
551
- "seeded_questions": 30,
552
- "swarm_enabled": true
553
- },
554
- "created_at": "2026-04-07T09:44:40+00:00",
555
- "episodes": 1,
556
- "metrics": {
557
- "avg_compactness_reward": 0.0,
558
- "avg_connectivity_gain_reward": 0.2,
559
- "avg_connectivity_reward": -0.15,
560
- "avg_diversity_reward": 0.12666666666666665,
561
- "avg_entity_informativeness_reward": -0.018704290877944903,
562
- "avg_format_reward": 0.15,
563
- "avg_graph_f1": 0.5714285714285715,
564
- "avg_knowledge_carrier_reward": 0.5,
565
- "avg_knowledge_indexing_reward": 0.12272727272727273,
566
- "avg_relation_informativeness_reward": 0.08056039127695382,
567
- "avg_reward": 0.7135379106634446,
568
- "avg_soft_shaping_reward": 0.3,
569
- "avg_spawn_count": 4.0,
570
- "avg_spawn_critical_steps": 6.0,
571
- "avg_steps_to_solution": 9.0,
572
- "deanonymization_accuracy": 0.0,
573
- "leaderboard_score": 0.6633913226563717,
574
- "retrieval_signal": 0.7179545454545455,
575
- "spawn_completion_rate": 1.0,
576
- "spawn_signal": 0.6666666666666666,
577
- "structural_signal": 0.5138712200798018,
578
- "task_success_rate": 1.0,
579
- "tool_efficiency": 0.5
580
- },
581
- "run_id": "run_0014",
582
- "run_name": "fixed_levels_qwen_swarm"
583
- },
584
- {
585
- "config": {
586
- "llm_model": "gpt-5.4-mini",
587
- "llm_provider": "openai",
588
- "max_agents": 3,
589
- "max_breadth": 2,
590
- "max_depth": 2,
591
- "max_steps": 24,
592
- "max_width": 2,
593
- "seed": 2026,
594
- "seeded_questions": 30,
595
- "swarm_enabled": true
596
- },
597
- "created_at": "2026-04-07T09:55:19+00:00",
598
- "episodes": 1,
599
- "metrics": {
600
- "avg_compactness_reward": 0.0,
601
- "avg_connectivity_gain_reward": 0.2,
602
- "avg_connectivity_reward": -0.15,
603
- "avg_diversity_reward": 0.12666666666666665,
604
- "avg_entity_informativeness_reward": -0.018704290877944903,
605
- "avg_format_reward": 0.15,
606
- "avg_graph_f1": 0.5714285714285715,
607
- "avg_knowledge_carrier_reward": 0.5,
608
- "avg_knowledge_indexing_reward": 0.12272727272727273,
609
- "avg_relation_informativeness_reward": 0.08056039127695382,
610
- "avg_reward": 0.7135379106634446,
611
- "avg_soft_shaping_reward": 0.3,
612
- "avg_spawn_count": 4.0,
613
- "avg_spawn_critical_steps": 6.0,
614
- "avg_steps_to_solution": 9.0,
615
- "deanonymization_accuracy": 0.0,
616
- "leaderboard_score": 0.6633913226563717,
617
- "retrieval_signal": 0.7179545454545455,
618
- "spawn_completion_rate": 1.0,
619
- "spawn_signal": 0.6666666666666666,
620
- "structural_signal": 0.5138712200798018,
621
- "task_success_rate": 1.0,
622
- "tool_efficiency": 0.5
623
- },
624
- "run_id": "run_0015",
625
- "run_name": "fixed_levels_qwen_swarm"
626
- },
627
- {
628
- "config": {
629
- "llm_model": "gpt-5.4-mini",
630
- "llm_provider": "openai",
631
- "max_agents": 3,
632
- "max_breadth": 2,
633
- "max_depth": 2,
634
- "max_steps": 24,
635
- "max_width": 2,
636
- "seed": 2026,
637
- "seeded_questions": 30,
638
- "swarm_enabled": true
639
- },
640
- "created_at": "2026-04-07T09:56:28+00:00",
641
- "episodes": 30,
642
- "metrics": {
643
- "avg_compactness_reward": 0.0,
644
- "avg_connectivity_gain_reward": 0.2000000000000001,
645
- "avg_connectivity_reward": 0.12999999999999998,
646
- "avg_diversity_reward": 0.12433333333333325,
647
- "avg_entity_informativeness_reward": -0.02515191749984708,
648
- "avg_format_reward": 0.15,
649
- "avg_graph_f1": 0.2916528337385394,
650
- "avg_knowledge_carrier_reward": 0.5,
651
- "avg_knowledge_indexing_reward": 0.11539120363588044,
652
- "avg_relation_informativeness_reward": 0.0769903534735767,
653
- "avg_reward": 0.7150555461096118,
654
- "avg_soft_shaping_reward": 0.3,
655
- "avg_spawn_count": 4.0,
656
- "avg_spawn_critical_steps": 6.0,
657
- "avg_steps_to_solution": 9.0,
658
- "deanonymization_accuracy": 0.0,
659
- "leaderboard_score": 0.6132407715455404,
660
- "retrieval_signal": 0.7153869212725582,
661
- "spawn_completion_rate": 1.0,
662
- "spawn_signal": 0.6666666666666666,
663
- "structural_signal": 0.5815176871947458,
664
- "task_success_rate": 1.0,
665
- "tool_efficiency": 0.5
666
- },
667
- "run_id": "run_0016",
668
- "run_name": "fixed_levels_qwen_swarm"
669
- },
670
- {
671
- "config": {
672
- "llm_model": "gpt-5.4-mini",
673
- "llm_provider": "openai",
674
- "max_agents": 3,
675
- "max_breadth": 2,
676
- "max_depth": 2,
677
- "max_steps": 24,
678
- "max_width": 2,
679
- "seed": 2026,
680
- "seeded_questions": 30,
681
- "swarm_enabled": true
682
- },
683
- "created_at": "2026-04-07T10:02:32+00:00",
684
- "episodes": 1,
685
- "metrics": {
686
- "avg_compactness_reward": 0.0,
687
- "avg_connectivity_gain_reward": 0.2,
688
- "avg_connectivity_reward": -0.15,
689
- "avg_diversity_reward": 0.12666666666666665,
690
- "avg_entity_informativeness_reward": -0.018704290877944903,
691
- "avg_format_reward": 0.15,
692
- "avg_graph_f1": 0.5714285714285715,
693
- "avg_knowledge_carrier_reward": 0.5,
694
- "avg_knowledge_indexing_reward": 0.12272727272727273,
695
- "avg_relation_informativeness_reward": 0.08056039127695382,
696
- "avg_reward": 0.7135379106634446,
697
- "avg_soft_shaping_reward": 0.3,
698
- "avg_spawn_count": 4.0,
699
- "avg_spawn_critical_steps": 6.0,
700
- "avg_steps_to_solution": 9.0,
701
- "deanonymization_accuracy": 0.0,
702
- "leaderboard_score": 0.6633913226563717,
703
- "retrieval_signal": 0.7179545454545455,
704
- "spawn_completion_rate": 1.0,
705
- "spawn_signal": 0.6666666666666666,
706
- "structural_signal": 0.5138712200798018,
707
- "task_success_rate": 1.0,
708
- "tool_efficiency": 0.5
709
- },
710
- "run_id": "run_0017",
711
- "run_name": "fixed_levels_qwen_swarm"
712
- },
713
- {
714
- "config": {
715
- "llm_model": "gpt-5.4-mini",
716
- "llm_provider": "openai",
717
- "max_agents": 3,
718
- "max_breadth": 2,
719
- "max_depth": 2,
720
- "max_steps": 24,
721
- "max_width": 2,
722
- "seed": 2026,
723
- "seeded_questions": 30,
724
- "swarm_enabled": true
725
- },
726
- "created_at": "2026-04-07T10:02:49+00:00",
727
- "episodes": 3,
728
- "metrics": {
729
- "avg_compactness_reward": 0.0,
730
- "avg_connectivity_gain_reward": 0.20000000000000004,
731
- "avg_connectivity_reward": -0.06666666666666667,
732
- "avg_diversity_reward": 0.13444444444444445,
733
- "avg_entity_informativeness_reward": -0.029992009599206938,
734
- "avg_format_reward": 0.15,
735
- "avg_graph_f1": 0.5793650793650794,
736
- "avg_knowledge_carrier_reward": 0.5,
737
- "avg_knowledge_indexing_reward": 0.10372960372960373,
738
- "avg_relation_informativeness_reward": 0.06898843512226,
739
- "avg_reward": 0.7133699465240085,
740
- "avg_soft_shaping_reward": 0.3,
741
- "avg_spawn_count": 4.0,
742
- "avg_spawn_critical_steps": 6.0,
743
- "avg_steps_to_solution": 9.0,
744
- "deanonymization_accuracy": 0.0,
745
- "leaderboard_score": 0.6656078661080486,
746
- "retrieval_signal": 0.7113053613053614,
747
- "spawn_completion_rate": 1.0,
748
- "spawn_signal": 0.6666666666666666,
749
- "structural_signal": 0.5312992851046106,
750
- "task_success_rate": 1.0,
751
- "tool_efficiency": 0.5
752
- },
753
- "run_id": "run_0018",
754
- "run_name": "fixed_levels_qwen_swarm"
755
- },
756
- {
757
- "config": {
758
- "llm_model": "gpt-5.4-mini",
759
- "llm_provider": "openai",
760
- "max_agents": 3,
761
- "max_breadth": 2,
762
- "max_depth": 2,
763
- "max_steps": 24,
764
- "max_width": 2,
765
- "seed": 2026,
766
- "seeded_questions": 30,
767
- "swarm_enabled": true
768
- },
769
- "created_at": "2026-04-07T10:04:53+00:00",
770
- "episodes": 3,
771
- "metrics": {
772
- "avg_compactness_reward": 0.0,
773
- "avg_connectivity_gain_reward": 0.20000000000000004,
774
- "avg_connectivity_reward": -0.06666666666666667,
775
- "avg_diversity_reward": 0.13444444444444445,
776
- "avg_entity_informativeness_reward": -0.029992009599206938,
777
- "avg_format_reward": 0.15,
778
- "avg_graph_f1": 0.5793650793650794,
779
- "avg_knowledge_carrier_reward": 0.5,
780
- "avg_knowledge_indexing_reward": 0.10372960372960373,
781
- "avg_relation_informativeness_reward": 0.06898843512226,
782
- "avg_reward": 0.7133699465240085,
783
- "avg_soft_shaping_reward": 0.3,
784
- "avg_spawn_count": 4.0,
785
- "avg_spawn_critical_steps": 6.0,
786
- "avg_steps_to_solution": 9.0,
787
- "deanonymization_accuracy": 0.0,
788
- "leaderboard_score": 0.6656078661080486,
789
- "retrieval_signal": 0.7113053613053614,
790
- "spawn_completion_rate": 1.0,
791
- "spawn_signal": 0.6666666666666666,
792
- "structural_signal": 0.5312992851046106,
793
- "task_success_rate": 1.0,
794
- "tool_efficiency": 0.5
795
- },
796
- "run_id": "run_0019",
797
- "run_name": "fixed_levels_qwen_swarm"
798
- },
799
- {
800
- "config": {
801
- "llm_model": "gpt-5.4-mini",
802
- "llm_provider": "openai",
803
- "max_agents": 3,
804
- "max_breadth": 2,
805
- "max_depth": 2,
806
- "max_steps": 24,
807
- "max_width": 2,
808
- "seed": 2026,
809
- "seeded_questions": 30,
810
- "swarm_enabled": true
811
- },
812
- "created_at": "2026-04-07T10:11:34+00:00",
813
- "episodes": 3,
814
- "metrics": {
815
- "avg_compactness_reward": 0.0,
816
- "avg_connectivity_gain_reward": 0.20000000000000004,
817
- "avg_connectivity_reward": -0.06666666666666667,
818
- "avg_diversity_reward": 0.13444444444444445,
819
- "avg_entity_informativeness_reward": -0.029992009599206938,
820
- "avg_format_reward": 0.15,
821
- "avg_graph_f1": 0.5793650793650794,
822
- "avg_knowledge_carrier_reward": 0.5,
823
- "avg_knowledge_indexing_reward": 0.10372960372960373,
824
- "avg_relation_informativeness_reward": 0.06898843512226,
825
- "avg_reward": 0.7133699465240085,
826
- "avg_soft_shaping_reward": 0.3,
827
- "avg_spawn_count": 4.0,
828
- "avg_spawn_critical_steps": 6.0,
829
- "avg_steps_to_solution": 9.0,
830
- "deanonymization_accuracy": 0.0,
831
- "leaderboard_score": 0.6656078661080486,
832
- "retrieval_signal": 0.7113053613053614,
833
- "spawn_completion_rate": 1.0,
834
- "spawn_signal": 0.6666666666666666,
835
- "structural_signal": 0.5312992851046106,
836
- "task_success_rate": 1.0,
837
- "tool_efficiency": 0.5
838
- },
839
- "run_id": "run_0020",
840
- "run_name": "fixed_levels_qwen_swarm"
841
- },
842
- {
843
- "config": {
844
- "llm_model": "gpt-5.4-mini",
845
- "llm_provider": "openai",
846
- "max_agents": 3,
847
- "max_breadth": 2,
848
- "max_depth": 2,
849
- "max_steps": 24,
850
- "max_width": 2,
851
- "seed": 2026,
852
- "seeded_questions": 30,
853
- "swarm_enabled": true
854
- },
855
- "created_at": "2026-04-07T10:29:54+00:00",
856
- "episodes": 3,
857
- "metrics": {
858
- "avg_compactness_reward": 0.0,
859
- "avg_connectivity_gain_reward": 0.0,
860
- "avg_connectivity_reward": 0.0,
861
- "avg_diversity_reward": 0.0,
862
- "avg_entity_informativeness_reward": 0.0,
863
- "avg_format_reward": 0.15,
864
- "avg_graph_f1": 0.0,
865
- "avg_knowledge_carrier_reward": 0.0,
866
- "avg_knowledge_indexing_reward": 0.0,
867
- "avg_relation_informativeness_reward": 0.0,
868
- "avg_reward": 0.5519400198339021,
869
- "avg_soft_shaping_reward": 0.0,
870
- "avg_spawn_count": 0.0,
871
- "avg_spawn_critical_steps": 0.0,
872
- "avg_steps_to_solution": 1.0,
873
- "deanonymization_accuracy": 0.0,
874
- "leaderboard_score": 0.2785970009916951,
875
- "retrieval_signal": 0.5,
876
- "spawn_completion_rate": 0.0,
877
- "spawn_signal": 0.4,
878
- "structural_signal": 0.5,
879
- "task_success_rate": 0.0,
880
- "tool_efficiency": 1.0
881
- },
882
- "run_id": "run_0021",
883
- "run_name": "fixed_levels_qwen_swarm"
884
- },
885
- {
886
- "config": {
887
- "llm_model": "gpt-5.4-mini",
888
- "llm_provider": "openai",
889
- "max_agents": 3,
890
- "max_breadth": 2,
891
- "max_depth": 2,
892
- "max_steps": 24,
893
- "max_width": 2,
894
- "seed": 2026,
895
- "seeded_questions": 30,
896
- "swarm_enabled": true
897
- },
898
- "created_at": "2026-04-07T15:59:20+00:00",
899
- "episodes": 1,
900
- "metrics": {
901
- "avg_compactness_reward": 0.0,
902
- "avg_connectivity_gain_reward": 0.0,
903
- "avg_connectivity_reward": 0.0,
904
- "avg_diversity_reward": 0.0,
905
- "avg_entity_informativeness_reward": 0.0,
906
- "avg_format_reward": 0.15,
907
- "avg_graph_f1": 0.0,
908
- "avg_knowledge_carrier_reward": 0.0,
909
- "avg_knowledge_indexing_reward": 0.0,
910
- "avg_relation_informativeness_reward": 0.0,
911
- "avg_reward": 0.5519400198339021,
912
- "avg_soft_shaping_reward": 0.0,
913
- "avg_spawn_count": 0.0,
914
- "avg_spawn_critical_steps": 0.0,
915
- "avg_steps_to_solution": 1.0,
916
- "deanonymization_accuracy": 0.0,
917
- "leaderboard_score": 0.2785970009916951,
918
- "retrieval_signal": 0.5,
919
- "spawn_completion_rate": 0.0,
920
- "spawn_signal": 0.4,
921
- "structural_signal": 0.5,
922
- "task_success_rate": 0.0,
923
- "tool_efficiency": 1.0
924
- },
925
- "run_id": "run_0022",
926
- "run_name": "fixed_levels_qwen_swarm"
927
- },
928
- {
929
- "config": {
930
- "llm_model": "gpt-5.4-mini",
931
- "llm_provider": "openai",
932
- "max_agents": 3,
933
- "max_breadth": 2,
934
- "max_depth": 2,
935
- "max_steps": 24,
936
- "max_width": 2,
937
- "seed": 2026,
938
- "seeded_questions": 30,
939
- "swarm_enabled": true
940
- },
941
- "created_at": "2026-04-08T04:25:00+00:00",
942
- "episodes": 1,
943
- "metrics": {
944
- "avg_compactness_reward": 0.0,
945
- "avg_connectivity_gain_reward": 0.0,
946
- "avg_connectivity_reward": 0.0,
947
- "avg_diversity_reward": 0.0,
948
- "avg_entity_informativeness_reward": 0.0,
949
- "avg_format_reward": 0.15,
950
- "avg_graph_f1": 0.0,
951
- "avg_knowledge_carrier_reward": 0.0,
952
- "avg_knowledge_indexing_reward": 0.0,
953
- "avg_relation_informativeness_reward": 0.0,
954
- "avg_reward": 0.5519400198339021,
955
- "avg_soft_shaping_reward": 0.0,
956
- "avg_spawn_count": 0.0,
957
- "avg_spawn_critical_steps": 0.0,
958
- "avg_steps_to_solution": 1.0,
959
- "deanonymization_accuracy": 0.0,
960
- "leaderboard_score": 0.2785970009916951,
961
- "retrieval_signal": 0.5,
962
- "spawn_completion_rate": 0.0,
963
- "spawn_signal": 0.4,
964
- "structural_signal": 0.5,
965
- "task_success_rate": 0.0,
966
- "tool_efficiency": 1.0
967
- },
968
- "run_id": "run_0023",
969
- "run_name": "fixed_levels_qwen_swarm"
970
- },
971
- {
972
- "config": {
973
- "llm_model": "gpt-5.4-mini",
974
- "llm_provider": "openai",
975
- "max_agents": 3,
976
- "max_breadth": 2,
977
- "max_depth": 2,
978
- "max_steps": 24,
979
- "max_width": 2,
980
- "seed": 2026,
981
- "seeded_questions": 30,
982
- "swarm_enabled": true
983
- },
984
- "created_at": "2026-04-08T04:28:07+00:00",
985
- "episodes": 1,
986
- "metrics": {
987
- "avg_compactness_reward": 0.0,
988
- "avg_connectivity_gain_reward": 0.0,
989
- "avg_connectivity_reward": 0.0,
990
- "avg_diversity_reward": 0.0,
991
- "avg_entity_informativeness_reward": 0.0,
992
- "avg_format_reward": 0.15,
993
- "avg_graph_f1": 0.0,
994
- "avg_knowledge_carrier_reward": 0.0,
995
- "avg_knowledge_indexing_reward": 0.0,
996
- "avg_relation_informativeness_reward": 0.0,
997
- "avg_reward": 0.5519400198339021,
998
- "avg_soft_shaping_reward": 0.0,
999
- "avg_spawn_count": 0.0,
1000
- "avg_spawn_critical_steps": 0.0,
1001
- "avg_steps_to_solution": 1.0,
1002
- "deanonymization_accuracy": 0.0,
1003
- "leaderboard_score": 0.2785970009916951,
1004
- "retrieval_signal": 0.5,
1005
- "spawn_completion_rate": 0.0,
1006
- "spawn_signal": 0.4,
1007
- "structural_signal": 0.5,
1008
- "task_success_rate": 0.0,
1009
- "tool_efficiency": 1.0
1010
- },
1011
- "run_id": "run_0024",
1012
- "run_name": "fixed_levels_qwen_swarm"
1013
- },
1014
- {
1015
- "config": {
1016
- "llm_model": "gpt-5.4-mini",
1017
- "llm_provider": "openai",
1018
- "max_agents": 3,
1019
- "max_breadth": 2,
1020
- "max_depth": 2,
1021
- "max_steps": 24,
1022
- "max_width": 2,
1023
- "seed": 2026,
1024
- "seeded_questions": 30,
1025
- "swarm_enabled": true
1026
- },
1027
- "created_at": "2026-04-08T04:39:32+00:00",
1028
- "episodes": 1,
1029
- "metrics": {
1030
- "avg_compactness_reward": 0.0,
1031
- "avg_connectivity_gain_reward": 0.0,
1032
- "avg_connectivity_reward": 0.0,
1033
- "avg_diversity_reward": 0.0,
1034
- "avg_entity_informativeness_reward": 0.0,
1035
- "avg_format_reward": 0.15,
1036
- "avg_graph_f1": 0.0,
1037
- "avg_knowledge_carrier_reward": 0.0,
1038
- "avg_knowledge_indexing_reward": 0.0,
1039
- "avg_relation_informativeness_reward": 0.0,
1040
- "avg_reward": 0.5519400198339021,
1041
- "avg_soft_shaping_reward": 0.0,
1042
- "avg_spawn_count": 0.0,
1043
- "avg_spawn_critical_steps": 0.0,
1044
- "avg_steps_to_solution": 1.0,
1045
- "deanonymization_accuracy": 0.0,
1046
- "leaderboard_score": 0.2785970009916951,
1047
- "retrieval_signal": 0.5,
1048
- "spawn_completion_rate": 0.0,
1049
- "spawn_signal": 0.4,
1050
- "structural_signal": 0.5,
1051
- "task_success_rate": 0.0,
1052
- "tool_efficiency": 1.0
1053
- },
1054
- "run_id": "run_0025",
1055
- "run_name": "fixed_levels_qwen_swarm"
1056
- },
1057
- {
1058
- "config": {
1059
- "llm_model": "gpt-5.4-mini",
1060
- "llm_provider": "openai",
1061
- "max_agents": 3,
1062
- "max_breadth": 2,
1063
- "max_depth": 2,
1064
- "max_steps": 24,
1065
- "max_width": 2,
1066
- "seed": 2026,
1067
- "seeded_questions": 30,
1068
- "swarm_enabled": true
1069
- },
1070
- "created_at": "2026-04-08T04:40:21+00:00",
1071
- "episodes": 30,
1072
- "metrics": {
1073
- "avg_compactness_reward": 0.0,
1074
- "avg_connectivity_gain_reward": 0.13333333333333336,
1075
- "avg_connectivity_reward": 0.09999999999999999,
1076
- "avg_diversity_reward": 0.03911111111111111,
1077
- "avg_entity_informativeness_reward": -0.00951758755541623,
1078
- "avg_format_reward": 0.15,
1079
- "avg_graph_f1": 0.08482743691314255,
1080
- "avg_knowledge_carrier_reward": 0.3333333333333333,
1081
- "avg_knowledge_indexing_reward": 0.0832325289772058,
1082
- "avg_relation_informativeness_reward": 0.024842289016879314,
1083
- "avg_reward": 0.6636425017249088,
1084
- "avg_soft_shaping_reward": 0.19999999999999993,
1085
- "avg_spawn_count": 2.6666666666666665,
1086
- "avg_spawn_critical_steps": 4.0,
1087
- "avg_steps_to_solution": 6.333333333333333,
1088
- "deanonymization_accuracy": 0.0,
1089
- "leaderboard_score": 0.4644798510150634,
1090
- "retrieval_signal": 0.6457980518086888,
1091
- "spawn_completion_rate": 1.0,
1092
- "spawn_signal": 0.7,
1093
- "structural_signal": 0.5472649402922927,
1094
- "task_success_rate": 0.6666666666666666,
1095
- "tool_efficiency": 0.5
1096
- },
1097
- "run_id": "run_0026",
1098
- "run_name": "fixed_levels_qwen_swarm"
1099
- },
1100
- {
1101
- "config": {
1102
- "llm_model": "gpt-5.4-mini",
1103
- "llm_provider": "openai",
1104
- "max_agents": 3,
1105
- "max_breadth": 2,
1106
- "max_depth": 2,
1107
- "max_steps": 24,
1108
- "max_width": 2,
1109
- "seed": 2026,
1110
- "seeded_questions": 30,
1111
- "swarm_enabled": true
1112
- },
1113
- "created_at": "2026-04-08T05:01:16+00:00",
1114
- "episodes": 10,
1115
- "metrics": {
1116
- "avg_compactness_reward": 0.0,
1117
- "avg_connectivity_gain_reward": 0.0,
1118
- "avg_connectivity_reward": 0.0,
1119
- "avg_diversity_reward": 0.0,
1120
- "avg_entity_informativeness_reward": 0.0,
1121
- "avg_format_reward": 0.14999999999999997,
1122
- "avg_graph_f1": 0.0,
1123
- "avg_knowledge_carrier_reward": 0.0,
1124
- "avg_knowledge_indexing_reward": 0.0,
1125
- "avg_relation_informativeness_reward": 0.0,
1126
- "avg_reward": 0.5519400198339021,
1127
- "avg_soft_shaping_reward": 0.0,
1128
- "avg_spawn_count": 0.0,
1129
- "avg_spawn_critical_steps": 0.0,
1130
- "avg_steps_to_solution": 1.0,
1131
- "deanonymization_accuracy": 0.0,
1132
- "leaderboard_score": 0.2785970009916951,
1133
- "retrieval_signal": 0.5,
1134
- "spawn_completion_rate": 0.0,
1135
- "spawn_signal": 0.4,
1136
- "structural_signal": 0.5,
1137
- "task_success_rate": 0.0,
1138
- "tool_efficiency": 1.0
1139
- },
1140
- "run_id": "run_0027",
1141
- "run_name": "fixed_levels_qwen_swarm"
1142
- },
1143
- {
1144
- "config": {
1145
- "llm_model": "gpt-5.4-mini",
1146
- "llm_provider": "openai",
1147
- "max_agents": 3,
1148
- "max_breadth": 2,
1149
- "max_depth": 2,
1150
- "max_steps": 24,
1151
- "max_width": 2,
1152
- "seed": 2026,
1153
- "seeded_questions": 30,
1154
- "swarm_enabled": true
1155
- },
1156
- "created_at": "2026-04-08T05:01:29+00:00",
1157
- "episodes": 10,
1158
- "metrics": {
1159
- "avg_compactness_reward": 0.0,
1160
- "avg_connectivity_gain_reward": 0.19999999999999998,
1161
- "avg_connectivity_reward": 0.06,
1162
- "avg_diversity_reward": 0.0,
1163
- "avg_entity_informativeness_reward": 0.0,
1164
- "avg_format_reward": 0.14999999999999997,
1165
- "avg_graph_f1": 0.18535980927285275,
1166
- "avg_knowledge_carrier_reward": 0.5,
1167
- "avg_knowledge_indexing_reward": 0.09575879120879122,
1168
- "avg_relation_informativeness_reward": 0.0,
1169
- "avg_reward": 0.7109638031154166,
1170
- "avg_soft_shaping_reward": 0.29999999999999993,
1171
- "avg_spawn_count": 4.0,
1172
- "avg_spawn_critical_steps": 6.0,
1173
- "avg_steps_to_solution": 9.0,
1174
- "deanonymization_accuracy": 0.0,
1175
- "leaderboard_score": 0.5866289994462388,
1176
- "retrieval_signal": 0.708515576923077,
1177
- "spawn_completion_rate": 1.0,
1178
- "spawn_signal": 0.6666666666666666,
1179
- "structural_signal": 0.535,
1180
- "task_success_rate": 1.0,
1181
- "tool_efficiency": 0.5
1182
- },
1183
- "run_id": "run_0028",
1184
- "run_name": "fixed_levels_qwen_swarm"
1185
- },
1186
- {
1187
- "config": {
1188
- "llm_model": "gpt-5.4-mini",
1189
- "llm_provider": "openai",
1190
- "max_agents": 3,
1191
- "max_breadth": 2,
1192
- "max_depth": 2,
1193
- "max_steps": 24,
1194
- "max_width": 2,
1195
- "seed": 2026,
1196
- "seeded_questions": 30,
1197
- "swarm_enabled": true
1198
- },
1199
- "created_at": "2026-04-08T05:01:43+00:00",
1200
- "episodes": 10,
1201
- "metrics": {
1202
- "avg_compactness_reward": 0.0,
1203
- "avg_connectivity_gain_reward": 0.19999999999999998,
1204
- "avg_connectivity_reward": 0.24,
1205
- "avg_diversity_reward": 0.11733333333333333,
1206
- "avg_entity_informativeness_reward": -0.028552762666248687,
1207
- "avg_format_reward": 0.14999999999999997,
1208
- "avg_graph_f1": 0.06912250146657492,
1209
- "avg_knowledge_carrier_reward": 0.5,
1210
- "avg_knowledge_indexing_reward": 0.15393879572282626,
1211
- "avg_relation_informativeness_reward": 0.07452686705063795,
1212
- "avg_reward": 0.7171006884027153,
1213
- "avg_soft_shaping_reward": 0.29999999999999993,
1214
- "avg_spawn_count": 4.0,
1215
- "avg_spawn_critical_steps": 6.0,
1216
- "avg_steps_to_solution": 9.0,
1217
- "deanonymization_accuracy": 0.0,
1218
- "leaderboard_score": 0.5730007362494549,
1219
- "retrieval_signal": 0.7288785785029892,
1220
- "spawn_completion_rate": 1.0,
1221
- "spawn_signal": 0.6666666666666666,
1222
- "structural_signal": 0.6067948208768779,
1223
- "task_success_rate": 1.0,
1224
- "tool_efficiency": 0.5
1225
- },
1226
- "run_id": "run_0029",
1227
- "run_name": "fixed_levels_qwen_swarm"
1228
- }
1229
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scratch/osint/datasets/fixed_levels/qwen_swarm_benchmark_fixed_levels.json DELETED
@@ -1,69 +0,0 @@
1
- {
2
- "dashboard": "datasets/fixed_levels/dashboard_fixed_levels.html",
3
- "record": {
4
- "config": {
5
- "max_agents": 3,
6
- "max_breadth": 2,
7
- "max_depth": 2,
8
- "max_steps": 20,
9
- "max_width": 2,
10
- "seed": 2026,
11
- "seeded_questions": 15,
12
- "swarm_enabled": true
13
- },
14
- "created_at": "2026-04-01T18:48:39+00:00",
15
- "episodes": 15,
16
- "metrics": {
17
- "avg_compactness_reward": 0.0,
18
- "avg_connectivity_gain_reward": 0.16666666666666666,
19
- "avg_connectivity_reward": 0.16999999999999998,
20
- "avg_diversity_reward": 0.1157777777777778,
21
- "avg_entity_informativeness_reward": -0.08858065677817137,
22
- "avg_format_reward": 0.14999999999999997,
23
- "avg_graph_f1": 0.8492063492063492,
24
- "avg_knowledge_carrier_reward": 0.5,
25
- "avg_knowledge_indexing_reward": 0.052000000000000005,
26
- "avg_relation_informativeness_reward": 0.07135858524047924,
27
- "avg_reward": 4.197526826881651,
28
- "avg_soft_shaping_reward": 0.24999999999999994,
29
- "avg_spawn_count": 4.0,
30
- "avg_spawn_critical_steps": 6.0,
31
- "avg_steps_to_solution": 9.0,
32
- "deanonymization_accuracy": 1.0,
33
- "leaderboard_score": 0.8543934355282199,
34
- "retrieval_signal": 0.6932,
35
- "spawn_completion_rate": 1.0,
36
- "spawn_signal": 0.6666666666666666,
37
- "structural_signal": 0.5730889190257948,
38
- "task_success_rate": 1.0,
39
- "tool_efficiency": 0.5
40
- },
41
- "run_id": "run_0001",
42
- "run_name": "fixed_levels_qwen_swarm"
43
- },
44
- "summary": {
45
- "avg_compactness_reward": 0.0,
46
- "avg_connectivity_gain_reward": 0.16666666666666666,
47
- "avg_connectivity_reward": 0.16999999999999998,
48
- "avg_diversity_reward": 0.1157777777777778,
49
- "avg_entity_informativeness_reward": -0.08858065677817137,
50
- "avg_format_reward": 0.14999999999999997,
51
- "avg_graph_f1": 0.8492063492063492,
52
- "avg_knowledge_carrier_reward": 0.5,
53
- "avg_knowledge_indexing_reward": 0.052000000000000005,
54
- "avg_relation_informativeness_reward": 0.07135858524047924,
55
- "avg_reward": 4.197526826881651,
56
- "avg_soft_shaping_reward": 0.24999999999999994,
57
- "avg_spawn_count": 4.0,
58
- "avg_spawn_critical_steps": 6.0,
59
- "avg_steps_to_solution": 9.0,
60
- "deanonymization_accuracy": 1.0,
61
- "leaderboard_score": 0.8543934355282199,
62
- "retrieval_signal": 0.6932,
63
- "spawn_completion_rate": 1.0,
64
- "spawn_signal": 0.6666666666666666,
65
- "structural_signal": 0.5730889190257948,
66
- "task_success_rate": 1.0,
67
- "tool_efficiency": 0.5
68
- }
69
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scratch/osint/datasets/fixed_levels/qwen_swarm_eval_by_difficulty.json DELETED
@@ -1,53 +0,0 @@
1
- {
2
- "by_difficulty": {
3
- "easy": {
4
- "avg_graph_f1": 1.0,
5
- "avg_reward": 3.610490808845623,
6
- "avg_steps": 9.0,
7
- "avg_tool_calls": 4.0,
8
- "episodes": 5,
9
- "task_success_rate": 1.0
10
- },
11
- "high": {
12
- "avg_graph_f1": 0.5476190476190477,
13
- "avg_reward": 4.207102815893519,
14
- "avg_steps": 9.0,
15
- "avg_tool_calls": 4.0,
16
- "episodes": 5,
17
- "task_success_rate": 1.0
18
- },
19
- "mid": {
20
- "avg_graph_f1": 1.0,
21
- "avg_reward": 4.822687547070801,
22
- "avg_steps": 9.0,
23
- "avg_tool_calls": 4.0,
24
- "episodes": 5,
25
- "task_success_rate": 1.0
26
- }
27
- },
28
- "overall": {
29
- "avg_compactness_reward": 0.0,
30
- "avg_connectivity_gain_reward": 0.16666666666666666,
31
- "avg_connectivity_reward": 0.16999999999999998,
32
- "avg_diversity_reward": 0.1157777777777778,
33
- "avg_entity_informativeness_reward": -0.07289878447762359,
34
- "avg_format_reward": 0.14999999999999997,
35
- "avg_graph_f1": 0.8492063492063492,
36
- "avg_knowledge_carrier_reward": 0.5,
37
- "avg_knowledge_indexing_reward": 0.052000000000000005,
38
- "avg_relation_informativeness_reward": 0.07157694332826091,
39
- "avg_reward": 4.213427057269981,
40
- "avg_soft_shaping_reward": 0.24999999999999994,
41
- "avg_spawn_count": 4.0,
42
- "avg_spawn_critical_steps": 6.0,
43
- "avg_steps_to_solution": 9.0,
44
- "deanonymization_accuracy": 1.0,
45
- "leaderboard_score": 0.8546911504342771,
46
- "retrieval_signal": 0.6932,
47
- "spawn_completion_rate": 1.0,
48
- "spawn_signal": 0.6666666666666666,
49
- "structural_signal": 0.5762689651034608,
50
- "task_success_rate": 1.0,
51
- "tool_efficiency": 0.5
52
- }
53
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scratch/osint/datasets/fixed_levels/qwen_swarm_eval_fixed_levels.json DELETED
@@ -1,25 +0,0 @@
1
- {
2
- "avg_compactness_reward": 0.0,
3
- "avg_connectivity_gain_reward": 0.16666666666666666,
4
- "avg_connectivity_reward": 0.16999999999999998,
5
- "avg_diversity_reward": 0.1157777777777778,
6
- "avg_entity_informativeness_reward": -0.02824631570420193,
7
- "avg_format_reward": 0.14999999999999997,
8
- "avg_graph_f1": 0.8492063492063492,
9
- "avg_knowledge_carrier_reward": 0.5,
10
- "avg_knowledge_indexing_reward": 0.07400000000000001,
11
- "avg_relation_informativeness_reward": 0.06905976285357758,
12
- "avg_reward": 4.285384567790942,
13
- "avg_soft_shaping_reward": 0.24999999999999994,
14
- "avg_spawn_count": 4.0,
15
- "avg_spawn_critical_steps": 6.0,
16
- "avg_steps_to_solution": 9.0,
17
- "deanonymization_accuracy": 1.0,
18
- "leaderboard_score": 0.8565775118852701,
19
- "retrieval_signal": 0.7009000000000001,
20
- "spawn_completion_rate": 1.0,
21
- "spawn_signal": 0.6666666666666666,
22
- "structural_signal": 0.5846960227632085,
23
- "task_success_rate": 1.0,
24
- "tool_efficiency": 0.5
25
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scratch/osint/datasets/fixed_levels/seed_fixed_levels.json DELETED
The diff for this file is too large to render. See raw diff
 
scratch/osint/datasets/fixed_levels/shared_config_fixed_levels.json DELETED
@@ -1,55 +0,0 @@
1
- {
2
- "environment": {
3
- "n_users": 24,
4
- "alias_density": 0.2,
5
- "noise_level": 0.12,
6
- "red_herring_rate": 0.08,
7
- "max_steps": 24,
8
- "seed": 2026
9
- },
10
- "swarm": {
11
- "enabled": true,
12
- "max_agents": 3,
13
- "max_breadth": 2,
14
- "max_width": 2,
15
- "max_depth": 2,
16
- "planner_rounds": 2,
17
- "tools_per_agent": 1
18
- },
19
- "spawn_reward": {
20
- "lambda_parallel": 0.15,
21
- "lambda_finish": 0.2,
22
- "anneal": 1.0,
23
- "max_parallel_hint": 3
24
- },
25
- "seeding": {
26
- "seeded_nodes": [],
27
- "seeded_edges": [],
28
- "seeded_questions": [],
29
- "llm_generate_remaining_graph": true,
30
- "llm_generate_remaining_tasks": false,
31
- "llm_generated_edge_budget": 64,
32
- "llm_generated_task_budget": 0,
33
- "llm_generation_parallel": true,
34
- "llm_generation_workers": 4,
35
- "llm_generation_retries": 3,
36
- "allow_template_fallback_on_llm_failure": false
37
- },
38
- "llm": {
39
- "provider": "ollama",
40
- "model": "qwen3:2b",
41
- "temperature": 0.05,
42
- "max_tokens": 384,
43
- "timeout_seconds": 240,
44
- "ollama_base_url": "http://127.0.0.1:11434",
45
- "openai_base_url": "https://api.openai.com/v1",
46
- "openai_api_key_env": "OPENAI_API_KEY",
47
- "openai_api_key": ""
48
- },
49
- "runtime": {
50
- "default_episodes": 30,
51
- "leaderboard_path": "datasets/fixed_levels/leaderboard_fixed_levels.json",
52
- "dashboard_path": "datasets/fixed_levels/dashboard_fixed_levels.html",
53
- "sweep_dashboard_dir": "datasets/fixed_levels/sweep_dashboards"
54
- }
55
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scratch/osint/docs/reward_design_notes.md DELETED
@@ -1,94 +0,0 @@
1
- # Reward Design Notes
2
-
3
- This environment uses a composite reward that adapts ideas from:
4
-
5
- - AutoGraph-R1 (arXiv:2510.15339)
6
- - UniRel (arXiv:2512.17043)
7
- - DeepPath (EMNLP 2017, D17-1060)
8
- - Multi-Hop KG Reasoning with Reward Shaping (EMNLP 2018, D18-1362)
9
- - Kimi K2.5 (arXiv:2602.02276) for PARL-style swarm auxiliary shaping
10
-
11
- Additional related context consulted:
12
-
13
- - MINERVA (arXiv:1711.05851) for query-conditioned walk-style reasoning over KG paths.
14
-
15
- ## Components in this Branch
16
-
17
- The implementation follows a staged reward design:
18
-
19
- 1. edge-level rewards during graph construction (`ADD_EDGE`)
20
- 2. answer-level rewards for retrieval usefulness and final task utility (`ANSWER`)
21
- 3. evaluation-level composite leaderboard score for benchmark ranking
22
-
23
- ### 1) Edge addition reward
24
-
25
- For each `ADD_EDGE`, the reward combines:
26
-
27
- - Global accuracy term (DeepPath):
28
- - $r_{global} = +1$ if a candidate edge is correct, else $-1$ (scaled in code for stability).
29
- - Soft shaping term (D18 reward shaping):
30
- - $R = R_b + (1 - R_b) f(s, r, o)$, where $f$ is a soft fact plausibility score.
31
- - In code, $f$ is approximated by relation/type priors plus small domain priors.
32
- - Efficiency term (DeepPath):
33
- - $r_{efficiency} \propto 1 / \text{step\_count}$.
34
- - Diversity term (DeepPath):
35
- - novelty from cosine dissimilarity of edge signatures; repeated patterns are down-weighted.
36
- - Relation/entity informativeness (UniRel):
37
- - relation rarity via normalized IDF of relation labels,
38
- - entity informativeness via inverse hub-penalty.
39
- - Connectivity gain term:
40
- - rewards bridge edges that connect previously disconnected graph regions.
41
-
42
- ### 2) Final answer reward
43
-
44
- For `ANSWER`, the reward combines:
45
-
46
- - format validity,
47
- - answer correctness,
48
- - knowledge-carrying utility (AutoGraph-R1 style):
49
- - $R_C(q, y, G) = \mathbb{{I}}[\text{{deducible}}(q, y \mid G)]$.
50
- - knowledge-indexing utility (AutoGraph-R1 style):
51
- - $R_I(q, D_{{gold}}, G) = |Top\text{{-}}k(G,q) \cap D_{{gold}}| / |D_{{gold}}|$,
52
- - approximated in this environment with evidence recall over tool outputs.
53
- - connectivity (UniRel style):
54
- - discrete connectivity reward over extracted seed entities, normalized for stable mixing.
55
- - graph F1 against supporting edges,
56
- - compactness penalty for unnecessary extra edges,
57
- - efficiency bonus,
58
- - relation/entity informativeness for the constructed subgraph,
59
- - repetition penalty to discourage redundant relation generation patterns.
60
-
61
- UniRel-style aggregate view represented in this branch:
62
-
63
- $$
64
- R(a) \approx R_{{fmt}} + R_{{con}} + w_1 R_{{ent}} + w_2 R_{{rel}} + \text{{task utility terms}}
65
- $$
66
-
67
- with task utility terms coming from AutoGraph-inspired $R_C$ and $R_I$ components.
68
-
69
- ## Telemetry
70
-
71
- Per-step component rewards are aggregated into `info["reward_components"]`, enabling:
72
-
73
- - richer benchmark summaries,
74
- - leaderboard ranking by composite utility,
75
- - visual diagnostics in dashboard exports.
76
-
77
- Evaluation also computes derived retrieval and structural utility signals used in leaderboard ranking.
78
-
79
- ## Future Multi-Agent Notes
80
-
81
- This branch now includes a low-width swarm baseline orchestrator that adds PARL-style auxiliary shaping on top of the core edge and answer rewards.
82
-
83
- The helper implementation is in:
84
-
85
- - `src/osint_env/env/spawn_reward_hooks.py`
86
-
87
- It follows the Kimi K2.5 style decomposition:
88
-
89
- - $r_{{PARL}}(x,y) = r_{{perf}}(x,y) + \lambda_1 r_{{parallel}} + \lambda_2 r_{{finish}}$,
90
- - optional critical-steps shaping for latency-sensitive training,
91
- - optional annealing of $\lambda_1, \lambda_2$ toward zero,
92
- - optional breadth/depth shaping hooks for future branch integration.
93
-
94
- The expanded project-level walkthrough is in `README.md` under "Reward Design (Integrated Notes)".
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scratch/osint/inference.py DELETED
@@ -1,509 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import json
4
- import os
5
- from pathlib import Path
6
- from typing import Any
7
-
8
- from osint_env.agents.single_agent import SingleAgentRunner
9
- from osint_env.agents.swarm_agent import SwarmAgentRunner
10
- from osint_env.config import clone_environment_config, load_seeding_config, load_shared_config
11
- from osint_env.domain.models import EnvironmentConfig
12
- from osint_env.env.environment import OSINTEnvironment
13
- from osint_env.env.reward import compute_graph_f1
14
- from osint_env.eval.leaderboard import append_leaderboard_record, load_leaderboard
15
- from osint_env.eval.metrics import EvalMetrics
16
- from osint_env.llm import build_llm_client
17
- from osint_env.viz import export_dashboard
18
-
19
-
20
- CONFIG_PATH = os.getenv("CONFIG_PATH", "datasets/fixed_levels/shared_config_fixed_levels.json")
21
- SEED_FILE = os.getenv("SEED_FILE", "datasets/fixed_levels/seed_fixed_levels.json")
22
- AGENT_MODE = os.getenv("AGENT_MODE", "swarm")
23
- LLM_PROVIDER = os.getenv("LLM_PROVIDER", "openai")
24
- MODEL_NAME = os.getenv("MODEL_NAME", "gpt-5.4")
25
- OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "")
26
- OPENAI_BASE_URL = os.getenv("OPENAI_BASE_URL", "")
27
- OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
28
- OPENAI_API_KEY_ENV = os.getenv("OPENAI_API_KEY_ENV", "OPENAI_API_KEY")
29
- API_BASE_URL = os.getenv("API_BASE_URL", "https://api.openai.com/v1")
30
- API_KEY = os.getenv("API_KEY", "")
31
- HF_SPACE_URL = os.getenv("HF_SPACE_URL", "")
32
- HF_TOKEN = os.getenv("HF_TOKEN","")
33
- LOCAL_IMAGE_NAME = os.getenv("LOCAL_IMAGE_NAME", "")
34
- LLM_TIMEOUT_SECONDS = int(os.getenv("LLM_TIMEOUT_SECONDS", "0"))
35
- EPISODES = int(os.getenv("EPISODES", "1"))
36
- SUCCESS_SCORE_THRESHOLD = float(os.getenv("SUCCESS_SCORE_THRESHOLD", "0.67"))
37
- TASK_INDICES_RAW = os.getenv("TASK_INDICES", "")
38
-
39
- WRITE_BENCHMARK_ARTIFACTS = os.getenv("WRITE_BENCHMARK_ARTIFACTS", "1").strip().lower() in {
40
- "1",
41
- "true",
42
- "yes",
43
- "y",
44
- "on",
45
- }
46
- LEADERBOARD_PATH = os.getenv("LEADERBOARD_PATH", "datasets/fixed_levels/leaderboard_fixed_levels.json")
47
- DASHBOARD_PATH = os.getenv("DASHBOARD_PATH", "datasets/fixed_levels/dashboard_fixed_levels.html")
48
- RUN_NAME = os.getenv("RUN_NAME", "fixed_levels_qwen_swarm")
49
-
50
- BENCHMARK = "osint-openenv"
51
- TASK_NAME = "fixed_levels_easy_mid_hard"
52
-
53
-
54
- def _parse_task_indices(raw: str) -> list[int]:
55
- out: list[int] = []
56
- for token in str(raw or "").split(","):
57
- stripped = token.strip()
58
- if not stripped:
59
- continue
60
- try:
61
- out.append(int(stripped))
62
- except ValueError:
63
- continue
64
- return out
65
-
66
-
67
- def _normalize_ollama_base_url(url: str) -> str:
68
- normalized = str(url or "").strip().rstrip("/")
69
- if normalized.endswith("/v1"):
70
- normalized = normalized[:-3].rstrip("/")
71
- return normalized or "http://127.0.0.1:11434"
72
-
73
-
74
- def _normalize_openai_base_url(url: str) -> str:
75
- normalized = str(url or "").strip().rstrip("/")
76
- if not normalized:
77
- return ""
78
- if normalized.endswith("/v1"):
79
- return normalized
80
- return f"{normalized}/v1"
81
-
82
-
83
- TASK_INDICES = _parse_task_indices(TASK_INDICES_RAW)
84
-
85
-
86
- def log_start(task: str, env: str, model: str) -> None:
87
- print(f"[START] task={task} env={env} model={model}", flush=True)
88
-
89
-
90
- def log_step(step: int, action: str, reward: float, done: bool, error: str | None) -> None:
91
- error_text = "null" if error is None else str(error)
92
- print(
93
- f"[STEP] step={step} action={action} reward={reward:.2f} done={str(bool(done)).lower()} error={error_text}",
94
- flush=True,
95
- )
96
-
97
-
98
- def log_end(task: str, success: bool, steps: int, score: float, rewards: list[float]) -> None:
99
- rewards_text = ",".join(f"{value:.2f}" for value in rewards)
100
- print(
101
- f"[END] success={str(bool(success)).lower()} steps={steps} score={score:.2f} rewards={rewards_text}",
102
- flush=True,
103
- )
104
-
105
-
106
- def _looks_like_placeholder_api_key(value: str) -> bool:
107
- token = str(value or "").strip().lower()
108
- if not token:
109
- return True
110
- placeholder_markers = [
111
- "your_openai_api_key",
112
- "your-key",
113
- "your_key",
114
- "your real",
115
- "real-openai-key",
116
- "replace-me",
117
- "changeme",
118
- "example",
119
- "<api-key>",
120
- ]
121
- if token.startswith("your_") or token.startswith("sk-your-"):
122
- return True
123
- return any(marker in token for marker in placeholder_markers)
124
-
125
-
126
- def _format_action(action: dict[str, Any]) -> str:
127
- action_type = str(action.get("action_type", "")).upper()
128
- payload = dict(action.get("payload", {}))
129
-
130
- if action_type == "ANSWER":
131
- return f"answer({str(payload.get('answer', 'unknown')).strip()})"
132
-
133
- if action_type == "ADD_EDGE":
134
- try:
135
- conf = float(payload.get("confidence", 1.0))
136
- except (TypeError, ValueError):
137
- conf = 1.0
138
- return (
139
- "add_edge("
140
- f"{payload.get('src', '')},"
141
- f"{payload.get('rel', '')},"
142
- f"{payload.get('dst', '')},"
143
- f"{conf:.2f}"
144
- ")"
145
- )
146
-
147
- tool_name = str(payload.get("tool_name", "tool")).strip() or "tool"
148
- args = payload.get("args", {})
149
- if not isinstance(args, dict) or not args:
150
- return f"{tool_name}()"
151
- args_text = ",".join(f"{key}={value}" for key, value in sorted(args.items()))
152
- return f"{tool_name}({args_text})"
153
-
154
-
155
- def _assistant_tool_call_id(message: dict[str, Any]) -> str | None:
156
- tool_calls = list(message.get("tool_calls", []))
157
- if not tool_calls:
158
- return None
159
- tool_call_id = tool_calls[0].get("id")
160
- return str(tool_call_id) if tool_call_id else None
161
-
162
-
163
- def _tool_result_message(assistant_message: dict[str, Any], result: dict[str, Any]) -> dict[str, Any] | None:
164
- tool_call_id = _assistant_tool_call_id(assistant_message)
165
- if not tool_call_id:
166
- return None
167
- return {
168
- "role": "tool",
169
- "tool_call_id": tool_call_id,
170
- "content": json.dumps(result, sort_keys=True),
171
- }
172
-
173
-
174
- def _resolve_environment_config() -> EnvironmentConfig:
175
- shared = load_shared_config(CONFIG_PATH)
176
- env_cfg = clone_environment_config(shared.environment)
177
-
178
- if SEED_FILE and Path(SEED_FILE).exists():
179
- env_cfg.seeding = load_seeding_config(SEED_FILE)
180
-
181
- mode = AGENT_MODE.strip().lower()
182
- if mode == "single":
183
- env_cfg.swarm.enabled = False
184
- elif mode == "swarm":
185
- env_cfg.swarm.enabled = True
186
-
187
- # Inference submissions must route all calls through OpenAI-compatible client config.
188
- env_cfg.llm.provider = "openai"
189
- env_cfg.llm.model = MODEL_NAME.strip()
190
-
191
- if LLM_TIMEOUT_SECONDS > 0:
192
- env_cfg.llm.timeout_seconds = int(LLM_TIMEOUT_SECONDS)
193
-
194
- # Evaluation harnesses inject API_BASE_URL/HF_TOKEN for proxy-enforced requests.
195
- resolved_openai_base = API_BASE_URL.strip() or OPENAI_BASE_URL.strip() or HF_SPACE_URL.strip()
196
- if resolved_openai_base:
197
- env_cfg.llm.openai_base_url = _normalize_openai_base_url(resolved_openai_base)
198
-
199
- if HF_TOKEN.strip():
200
- env_cfg.llm.openai_api_key = HF_TOKEN.strip()
201
- elif API_KEY.strip():
202
- env_cfg.llm.openai_api_key = API_KEY.strip()
203
- elif OPENAI_API_KEY.strip():
204
- env_cfg.llm.openai_api_key = OPENAI_API_KEY.strip()
205
-
206
- if OPENAI_API_KEY_ENV.strip():
207
- env_cfg.llm.openai_api_key_env = OPENAI_API_KEY_ENV.strip()
208
-
209
- return env_cfg
210
-
211
-
212
- def _runner_for(env: OSINTEnvironment, llm: Any) -> SingleAgentRunner | SwarmAgentRunner:
213
- if env.config.swarm.enabled:
214
- return SwarmAgentRunner(env=env, llm=llm)
215
- return SingleAgentRunner(env=env, llm=llm)
216
-
217
-
218
- def _normalize_difficulty(value: str) -> str:
219
- token = str(value or "").strip().lower()
220
- if token in {"easy", "e"}:
221
- return "easy"
222
- if token in {"mid", "medium", "m"}:
223
- return "medium"
224
- if token in {"high", "hard", "h"}:
225
- return "hard"
226
- return "hard"
227
-
228
-
229
- def _task_difficulty(env: OSINTEnvironment, task_index: int) -> str:
230
- idx = int(task_index) % max(1, len(env.tasks))
231
- task = env.tasks[idx]
232
- if isinstance(task.metadata, dict) and "difficulty" in task.metadata:
233
- return _normalize_difficulty(str(task.metadata.get("difficulty", "")))
234
- if idx < 10:
235
- return "easy"
236
- if idx < 20:
237
- return "medium"
238
- return "hard"
239
-
240
-
241
- def _episode_row(env: OSINTEnvironment, info: dict[str, Any]) -> dict[str, Any]:
242
- if env.state is None:
243
- return {
244
- "task_id": "unknown",
245
- "task_type": "unknown",
246
- "question": "",
247
- "task_answer": str(info.get("task_answer", "")),
248
- "agent_answer": str(info.get("agent_answer", "")),
249
- "graph_f1": 0.0,
250
- "reward": float(info.get("total_reward", 0.0) or 0.0),
251
- "steps": int(info.get("step_count", 0) or 0),
252
- "tool_calls": int(info.get("tool_calls", 0) or 0),
253
- "success": int(info.get("agent_answer") == info.get("task_answer")),
254
- "reward_components": dict(info.get("reward_components", {})),
255
- "pred_edges": [],
256
- "truth_edges": [],
257
- }
258
-
259
- graph_f1 = compute_graph_f1(env.memory_graph.edges, env.state.task.supporting_edges)
260
- return {
261
- "task_id": env.state.task.task_id,
262
- "task_type": env.state.task.task_type,
263
- "question": env.state.task.question,
264
- "task_answer": str(info.get("task_answer", "")),
265
- "agent_answer": str(info.get("agent_answer", "")) if info.get("agent_answer") is not None else "",
266
- "graph_f1": graph_f1,
267
- "reward": float(info.get("total_reward", 0.0) or 0.0),
268
- "steps": int(info.get("step_count", 0) or 0),
269
- "tool_calls": int(info.get("tool_calls", 0) or 0),
270
- "success": int(info.get("agent_answer") == info.get("task_answer")),
271
- "reward_components": dict(info.get("reward_components", {})),
272
- "spawn_count": int(info.get("spawn_count", 0) or 0),
273
- "spawn_critical_steps": int(info.get("spawn_critical_steps", 0) or 0),
274
- "pred_edges": [
275
- {
276
- "src": edge.src,
277
- "rel": edge.rel,
278
- "dst": edge.dst,
279
- "confidence": float(edge.confidence),
280
- }
281
- for edge in env.memory_graph.edges
282
- ],
283
- "truth_edges": [
284
- {
285
- "src": edge.src,
286
- "rel": edge.rel,
287
- "dst": edge.dst,
288
- "confidence": float(edge.confidence),
289
- }
290
- for edge in env.state.task.supporting_edges
291
- ],
292
- }
293
-
294
-
295
- def _last_action_error(observation: Any, info: dict[str, Any]) -> str | None:
296
- raw = info.get("last_action_error") if isinstance(info, dict) else None
297
- if raw is not None:
298
- return str(raw)
299
-
300
- tool_outputs = getattr(observation, "tool_outputs", None)
301
- if isinstance(tool_outputs, list) and tool_outputs:
302
- last = tool_outputs[-1]
303
- if isinstance(last, dict):
304
- output = last.get("output")
305
- if isinstance(output, dict) and output.get("error") is not None:
306
- return str(output.get("error"))
307
- return None
308
-
309
-
310
- def _install_step_logger(env: OSINTEnvironment) -> tuple[list[float], dict[str, int], Any]:
311
- rewards: list[float] = []
312
- counters = {"steps": 0}
313
- original_step = env.step
314
-
315
- def _logged_step(action: Any):
316
- observation, reward, done, info = original_step(action)
317
- counters["steps"] += 1
318
- reward_value = float(reward or 0.0)
319
- rewards.append(reward_value)
320
- action_type = getattr(action, "action_type", "")
321
- action_type_value = str(getattr(action_type, "value", action_type))
322
- action_text = _format_action(
323
- {
324
- "action_type": action_type_value,
325
- "payload": dict(getattr(action, "payload", {}) or {}),
326
- }
327
- )
328
- log_step(
329
- step=counters["steps"],
330
- action=action_text,
331
- reward=reward_value,
332
- done=bool(done),
333
- error=_last_action_error(observation, info if isinstance(info, dict) else {}),
334
- )
335
- return observation, reward, done, info
336
-
337
- env.step = _logged_step
338
- return rewards, counters, original_step
339
-
340
-
341
- def _validate_required_configuration() -> None:
342
- missing: list[str] = []
343
-
344
- api_base = API_BASE_URL.strip()
345
- model_name = MODEL_NAME.strip()
346
- hf_token = HF_TOKEN.strip()
347
- api_key = API_KEY.strip()
348
- openai_key = OPENAI_API_KEY.strip()
349
-
350
- if not api_base or api_base == "<your-active-endpoint>":
351
- missing.append("API_BASE_URL")
352
- if not model_name or model_name == "<your-active-model>":
353
- missing.append("MODEL_NAME")
354
- if not (hf_token or api_key or openai_key):
355
- missing.append("HF_TOKEN|API_KEY|OPENAI_API_KEY")
356
-
357
- # Required when using docker-image based env construction.
358
- if os.getenv("REQUIRE_LOCAL_IMAGE_NAME", "0").strip().lower() in {"1", "true", "yes", "on"}:
359
- if not LOCAL_IMAGE_NAME.strip():
360
- missing.append("LOCAL_IMAGE_NAME")
361
-
362
- if missing:
363
- raise RuntimeError(f"Missing required environment variables: {', '.join(sorted(set(missing)))}")
364
-
365
-
366
- def _task_targets(env: OSINTEnvironment, episodes: int, task_indices: list[int]) -> list[int | None]:
367
- if task_indices:
368
- task_count = max(1, len(env.tasks))
369
- return [index % task_count for index in task_indices]
370
- return [None] * max(1, episodes)
371
-
372
-
373
- def _run_with_runner(
374
- env: OSINTEnvironment,
375
- llm: Any,
376
- episodes: int,
377
- task_indices: list[int],
378
- ) -> tuple[dict[str, Any], list[dict[str, Any]], list[float], int]:
379
- metrics = EvalMetrics()
380
- episode_rows: list[dict[str, Any]] = []
381
- rewards, counters, original_step = _install_step_logger(env)
382
-
383
- single_runner = SingleAgentRunner(env=env, llm=llm)
384
- swarm_runner = SwarmAgentRunner(env=env, llm=llm) if env.config.swarm.enabled else None
385
-
386
- try:
387
- for task_index in _task_targets(env, episodes, task_indices):
388
- task_count = max(1, len(env.tasks))
389
- selected_index = env._task_idx % task_count if task_index is None else int(task_index) % task_count
390
- if task_index is not None:
391
- # Keep compatibility with explicit task selection from the previous inference script.
392
- env._task_idx = selected_index
393
-
394
- difficulty = _task_difficulty(env, selected_index)
395
- if difficulty == "easy":
396
- runner: SingleAgentRunner | SwarmAgentRunner = single_runner
397
- elif swarm_runner is not None:
398
- runner = swarm_runner
399
- else:
400
- runner = single_runner
401
-
402
- info = runner.run_episode()
403
- if env.state is None:
404
- continue
405
-
406
- graph_f1 = compute_graph_f1(env.memory_graph.edges, env.state.task.supporting_edges)
407
- metrics.add(info, task_type=env.state.task.task_type, graph_f1=graph_f1)
408
- episode_rows.append(_episode_row(env, info))
409
- finally:
410
- env.step = original_step
411
-
412
- return metrics.summary(), episode_rows, rewards, int(counters["steps"])
413
-
414
-
415
- def _maybe_write_artifacts(
416
- env: OSINTEnvironment,
417
- summary: dict[str, Any],
418
- episodes: int,
419
- episode_rows: list[dict[str, Any]],
420
- ) -> tuple[dict[str, Any] | None, str | None]:
421
- if not WRITE_BENCHMARK_ARTIFACTS:
422
- return None, None
423
-
424
- record = append_leaderboard_record(
425
- path=LEADERBOARD_PATH,
426
- summary=summary,
427
- episodes=episodes,
428
- run_name=RUN_NAME or None,
429
- config={
430
- "seed": env.config.seed,
431
- "max_steps": env.config.max_steps,
432
- "swarm_enabled": env.config.swarm.enabled,
433
- "max_agents": env.config.swarm.max_agents,
434
- "max_breadth": env.config.swarm.max_breadth,
435
- "max_width": env.config.swarm.max_width,
436
- "max_depth": env.config.swarm.max_depth,
437
- "seeded_questions": len(env.config.seeding.seeded_questions),
438
- "llm_provider": env.config.llm.provider,
439
- "llm_model": env.config.llm.model,
440
- },
441
- )
442
-
443
- leaderboard = load_leaderboard(LEADERBOARD_PATH)
444
- dashboard = export_dashboard(
445
- env=env,
446
- evaluation={"summary": summary, "episodes": episode_rows},
447
- leaderboard_records=leaderboard,
448
- output_path=DASHBOARD_PATH,
449
- )
450
- return record, dashboard
451
-
452
-
453
- def main() -> None:
454
- _validate_required_configuration()
455
- env_cfg = _resolve_environment_config()
456
- llm_client = build_llm_client(env_cfg.llm)
457
-
458
- episodes_given = "EPISODES" in os.environ and str(os.getenv("EPISODES", "")).strip() != ""
459
- task_indices_given = bool(TASK_INDICES)
460
-
461
- if not episodes_given and not task_indices_given:
462
- runs: list[tuple[str, list[int], int]] = [
463
- ("easy", list(range(0, 10)), 10),
464
- ("mid", list(range(10, 20)), 10),
465
- ("hard", list(range(20, 30)), 10),
466
- ]
467
- else:
468
- selected_indices = TASK_INDICES if task_indices_given else []
469
- episodes = len(selected_indices) if selected_indices else max(1, EPISODES)
470
- runs = [(TASK_NAME, selected_indices, episodes)]
471
-
472
- for task_name, run_indices, run_episodes in runs:
473
- env: OSINTEnvironment | None = None
474
- rewards: list[float] = []
475
- steps_taken = 0
476
- score = 0.0
477
- success = False
478
-
479
- env = OSINTEnvironment(env_cfg, llm=llm_client)
480
- log_start(task=task_name, env=BENCHMARK, model=env_cfg.llm.model)
481
-
482
- try:
483
- summary, episode_rows, rewards, steps_taken = _run_with_runner(
484
- env=env,
485
- llm=llm_client,
486
- episodes=run_episodes,
487
- task_indices=run_indices,
488
- )
489
-
490
- score = float(summary.get("avg_reward", 0.0) or 0.0)
491
- score = max(0.0, min(1.0, score))
492
- success = score >= SUCCESS_SCORE_THRESHOLD
493
-
494
- _maybe_write_artifacts(
495
- env=env,
496
- summary=summary,
497
- episodes=run_episodes,
498
- episode_rows=episode_rows,
499
- )
500
- finally:
501
- if env is not None:
502
- close_fn = getattr(env, "close", None)
503
- if callable(close_fn):
504
- close_fn()
505
- log_end(task=task_name, success=success, steps=steps_taken, score=score, rewards=rewards)
506
-
507
-
508
- if __name__ == "__main__":
509
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scratch/osint/my_env_v4.py DELETED
@@ -1,46 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass
4
-
5
-
6
- @dataclass(slots=True)
7
- class MyEnvV4Action:
8
- message: str
9
-
10
-
11
- @dataclass(slots=True)
12
- class _EchoObservation:
13
- echoed_message: str
14
-
15
-
16
- @dataclass(slots=True)
17
- class _EchoResult:
18
- observation: _EchoObservation
19
- reward: float = 0.0
20
- done: bool = False
21
-
22
-
23
- class MyEnvV4Env:
24
- def __init__(self) -> None:
25
- self._step_count = 0
26
-
27
- @classmethod
28
- async def from_docker_image(cls, image_name: str | None = None) -> "MyEnvV4Env":
29
- return cls()
30
-
31
- async def reset(self) -> _EchoResult:
32
- self._step_count = 0
33
- return _EchoResult(observation=_EchoObservation(echoed_message=""), reward=0.0, done=False)
34
-
35
- async def step(self, action: MyEnvV4Action) -> _EchoResult:
36
- self._step_count += 1
37
- message = str(getattr(action, "message", ""))
38
- reward = len(message) * 0.1
39
- return _EchoResult(
40
- observation=_EchoObservation(echoed_message=message),
41
- reward=reward,
42
- done=False,
43
- )
44
-
45
- async def close(self) -> None:
46
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scratch/osint/openenv.yaml DELETED
@@ -1,66 +0,0 @@
1
- name: osint-openenv
2
- version: 0.1.0
3
- description: Synthetic OSINT benchmark environment exposed over HTTP.
4
- tasks:
5
- - id: seed_task_0
6
- difficulty: easy
7
- max_steps: 24
8
- grader:
9
- type: difficulty_exact_match
10
- answer_type: node_id
11
- case_sensitive: true
12
- reward_profile: easy
13
- - id: seed_task_10
14
- difficulty: medium
15
- max_steps: 24
16
- grader:
17
- type: difficulty_exact_match
18
- answer_type: node_id
19
- case_sensitive: true
20
- reward_profile: medium
21
- - id: seed_task_20
22
- difficulty: hard
23
- max_steps: 24
24
- grader:
25
- type: difficulty_exact_match
26
- answer_type: node_id
27
- case_sensitive: true
28
- reward_profile: hard
29
- transport:
30
- type: http
31
- base_path: /
32
- endpoints:
33
- health:
34
- method: GET
35
- path: /health
36
- metadata:
37
- method: GET
38
- path: /api/environment
39
- tasks:
40
- method: GET
41
- path: /openenv/tasks
42
- reset:
43
- method: POST
44
- path: /reset
45
- step:
46
- method: POST
47
- path: /step
48
- state:
49
- method: GET
50
- path: /state
51
- models:
52
- action_space:
53
- - CALL_TOOL
54
- - ADD_EDGE
55
- - ANSWER
56
- task_fields:
57
- - task_id
58
- - task_type
59
- - question
60
- - difficulty
61
- - grader
62
- observation_fields:
63
- - tool_outputs
64
- - graph_snapshot
65
- - action_history
66
- - task
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scratch/osint/pyproject.toml DELETED
@@ -1,35 +0,0 @@
1
- [project]
2
- name = "osint-rl-env"
3
- version = "0.1.0"
4
- description = "OSINT-style multi-platform information ecosystem environment for LLM agents."
5
- readme = "README.md"
6
- requires-python = ">=3.10"
7
- dependencies = [
8
- "openenv>=0.1.13",
9
- "openai>=1.40.0",
10
- "fastapi>=0.115.0",
11
- "requests>=2.32.3",
12
- "uvicorn>=0.30.0",
13
- ]
14
-
15
- [project.optional-dependencies]
16
- dev = [
17
- "pytest>=8.0.0",
18
- ]
19
-
20
- [project.scripts]
21
- osint-env = "osint_env.cli:main"
22
- server = "osint_env.server_entry:main"
23
-
24
- [build-system]
25
- requires = ["setuptools>=68", "wheel"]
26
- build-backend = "setuptools.build_meta"
27
-
28
- [tool.setuptools]
29
- package-dir = {"" = "src"}
30
-
31
- [tool.setuptools.packages.find]
32
- where = ["src"]
33
-
34
- [tool.pytest.ini_options]
35
- testpaths = ["tests"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scratch/osint/requirements.txt DELETED
@@ -1,6 +0,0 @@
1
- openenv>=0.1.13
2
- openai>=1.40.0
3
- fastapi>=0.115.0
4
- requests>=2.32.3
5
- uvicorn>=0.30.0
6
- pytest>=8.0.0
 
 
 
 
 
 
 
scratch/osint/scripts/build_fixed_levels_dataset.py DELETED
@@ -1,197 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import argparse
4
- import json
5
- from collections import Counter
6
- from dataclasses import asdict
7
- from pathlib import Path
8
- from typing import Any
9
-
10
- from osint_env.config import clone_environment_config, load_seeding_config, load_shared_config
11
- from osint_env.data.generator import DatasetGenerator
12
- from osint_env.domain.models import Edge, TaskInstance
13
- from osint_env.llm import build_llm_client
14
-
15
-
16
- def edge_to_dict(edge: Edge) -> dict[str, Any]:
17
- return {
18
- "src": edge.src,
19
- "rel": edge.rel,
20
- "dst": edge.dst,
21
- "confidence": float(edge.confidence),
22
- }
23
-
24
-
25
- def task_to_dict(task: TaskInstance) -> dict[str, Any]:
26
- return {
27
- "task_id": task.task_id,
28
- "task_type": task.task_type,
29
- "question": task.question,
30
- "answer": task.answer,
31
- "supporting_edges": [edge_to_dict(e) for e in task.supporting_edges],
32
- "metadata": dict(task.metadata),
33
- }
34
-
35
-
36
- def build_fixed_snapshot(seed_path: Path) -> dict[str, Any]:
37
- seeding = load_seeding_config(seed_path)
38
- fixed_nodes = []
39
- for node in seeding.seeded_nodes:
40
- fixed_nodes.append(
41
- {
42
- "node_id": node.node_id,
43
- "node_type": str(getattr(node.node_type, "value", node.node_type)),
44
- "attrs": dict(node.attrs),
45
- }
46
- )
47
- fixed_edges = [
48
- {
49
- "src": edge.src,
50
- "rel": edge.rel,
51
- "dst": edge.dst,
52
- "confidence": float(edge.confidence),
53
- }
54
- for edge in seeding.seeded_edges
55
- ]
56
- fixed_questions = []
57
- for idx, q in enumerate(seeding.seeded_questions):
58
- fixed_questions.append(
59
- {
60
- "task_id": f"fixed_task_{idx:02d}",
61
- "task_type": q.task_type,
62
- "question": q.question,
63
- "answer": q.answer,
64
- "supporting_edges": [
65
- {
66
- "src": edge.src,
67
- "rel": edge.rel,
68
- "dst": edge.dst,
69
- "confidence": float(edge.confidence),
70
- }
71
- for edge in q.supporting_edges
72
- ],
73
- "metadata": dict(q.metadata),
74
- }
75
- )
76
-
77
- difficulty_counts = Counter(str(q.get("metadata", {}).get("difficulty", "unknown")) for q in fixed_questions)
78
- return {
79
- "dataset_name": "fixed_levels_submission_set",
80
- "source_seed": str(seed_path),
81
- "graph": {
82
- "nodes": fixed_nodes,
83
- "edges": fixed_edges,
84
- "node_count": len(fixed_nodes),
85
- "edge_count": len(fixed_edges),
86
- },
87
- "questions": fixed_questions,
88
- "question_count": len(fixed_questions),
89
- "difficulty_counts": dict(difficulty_counts),
90
- }
91
-
92
-
93
- def build_complete_snapshot(shared_config_path: Path, seed_path: Path) -> dict[str, Any]:
94
- shared = load_shared_config(shared_config_path)
95
- env_cfg = clone_environment_config(shared.environment)
96
- env_cfg.seeding = load_seeding_config(seed_path)
97
-
98
- llm_client = build_llm_client(env_cfg.llm)
99
- generator = DatasetGenerator(config=env_cfg, llm=llm_client)
100
-
101
- graph = generator.build_canonical_graph()
102
- views = generator.build_platform_views(graph)
103
- tasks = generator.generate_tasks(graph, views, count=max(15, len(env_cfg.seeding.seeded_questions)))
104
-
105
- difficulty_counts = Counter(str(task.metadata.get("difficulty", "unknown")) for task in tasks)
106
-
107
- return {
108
- "dataset_name": "fixed_levels_submission_set",
109
- "generation_mode": "llm_expanded",
110
- "shared_config": str(shared_config_path),
111
- "seed_file": str(seed_path),
112
- "llm": asdict(env_cfg.llm),
113
- "environment": {
114
- "n_users": env_cfg.n_users,
115
- "alias_density": env_cfg.alias_density,
116
- "noise_level": env_cfg.noise_level,
117
- "red_herring_rate": env_cfg.red_herring_rate,
118
- "seed": env_cfg.seed,
119
- },
120
- "canonical_graph": {
121
- "node_count": len(graph.nodes),
122
- "edge_count": len(graph.edges),
123
- "nodes": [
124
- {
125
- "node_id": node.node_id,
126
- "node_type": node.node_type.value,
127
- "attrs": dict(node.attrs),
128
- }
129
- for node in sorted(graph.nodes.values(), key=lambda n: n.node_id)
130
- ],
131
- "edges": [edge_to_dict(edge) for edge in graph.edges],
132
- },
133
- "platform_views": {
134
- "microblog_posts": views.microblog_posts,
135
- "forum_threads": views.forum_threads,
136
- "profiles": views.profiles,
137
- "counts": {
138
- "microblog_posts": len(views.microblog_posts),
139
- "forum_threads": len(views.forum_threads),
140
- "profiles": len(views.profiles),
141
- },
142
- },
143
- "tasks": [task_to_dict(task) for task in tasks],
144
- "task_count": len(tasks),
145
- "difficulty_counts": dict(difficulty_counts),
146
- }
147
-
148
-
149
- def main() -> None:
150
- parser = argparse.ArgumentParser(description="Build fixed difficulty dataset artifacts.")
151
- parser.add_argument(
152
- "--seed-file",
153
- default="datasets/fixed_levels/seed_fixed_levels.json",
154
- help="Path to seeding JSON with fixed graph/questions.",
155
- )
156
- parser.add_argument(
157
- "--shared-config",
158
- default="datasets/fixed_levels/shared_config_fixed_levels.json",
159
- help="Path to shared config used for LLM-expanded generation.",
160
- )
161
- parser.add_argument(
162
- "--output-dir",
163
- default="datasets/fixed_levels",
164
- help="Directory where dataset artifacts are written.",
165
- )
166
- args = parser.parse_args()
167
-
168
- output_dir = Path(args.output_dir)
169
- output_dir.mkdir(parents=True, exist_ok=True)
170
-
171
- seed_path = Path(args.seed_file)
172
- shared_path = Path(args.shared_config)
173
-
174
- fixed_snapshot = build_fixed_snapshot(seed_path)
175
- fixed_path = output_dir / "fixed_graph_questions.json"
176
- fixed_path.write_text(json.dumps(fixed_snapshot, indent=2, sort_keys=True), encoding="utf-8")
177
-
178
- complete_snapshot = build_complete_snapshot(shared_path, seed_path)
179
- complete_path = output_dir / "complete_dataset_qwen_generated.json"
180
- complete_path.write_text(json.dumps(complete_snapshot, indent=2, sort_keys=True), encoding="utf-8")
181
-
182
- summary = {
183
- "fixed_dataset": str(fixed_path),
184
- "complete_dataset": str(complete_path),
185
- "fixed_nodes": fixed_snapshot["graph"]["node_count"],
186
- "fixed_edges": fixed_snapshot["graph"]["edge_count"],
187
- "fixed_questions": fixed_snapshot["question_count"],
188
- "complete_nodes": complete_snapshot["canonical_graph"]["node_count"],
189
- "complete_edges": complete_snapshot["canonical_graph"]["edge_count"],
190
- "complete_tasks": complete_snapshot["task_count"],
191
- "difficulty_counts": complete_snapshot["difficulty_counts"],
192
- }
193
- print(json.dumps(summary, indent=2, sort_keys=True))
194
-
195
-
196
- if __name__ == "__main__":
197
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scratch/osint/scripts/generate_fixed_levels_seed.py DELETED
@@ -1,109 +0,0 @@
1
- from collections import Counter, OrderedDict
2
- from pathlib import Path
3
- import json
4
-
5
- U=[('aria','Aria Sen','Helios Labs','Sector 9'),('bharat','Bharat Kulkarni','Northbridge Logistics','Dockyard 17'),('cyrus','Cyrus Mehta','Apex Dynamics','Old Town'),('diya','Diya Roy','Blueharbor Media','Old Town'),('elin','Elin Das','Helios Labs','Sector 9'),('faris','Faris Noor','Tidewatch Ops','Rivergate'),('gita','Gita Pradhan','Apex Dynamics','Old Town'),('hiro','Hiro Tan','Northbridge Logistics','Dockyard 17'),('ivy','Ivy Kapoor','Kestrel Works','Rivergate'),('jules','Jules Banerjee','Blueharbor Media','Old Town'),('kian','Kian Bose','Atlas Freight','East Quay'),('leena','Leena Das','Sunmesh Analytics','Sector 9'),('mika','Mika Solanki','Orion Customs','North Basin'),('nora','Nora Iqbal','Emberline Security','Foundry Row'),('omar','Omar Sheikh','Atlas Freight','East Quay'),('priya','Priya Menon','Sunmesh Analytics','Sector 9'),('quinn','Quinn Rao','Orion Customs','North Basin'),('rhea','Rhea Kapoor','Emberline Security','Foundry Row'),('soren','Soren Malik','Harborlight Transit','Uplink Yard'),('tara','Tara Dey','Harborlight Transit','Uplink Yard')]
6
- A=[('orchidfox','@orchidfox','ivy'),('steelquill','@steelquill','bharat'),('monsoonbyte','@monsoonbyte','diya'),('nightrelay','@nightrelay','faris'),('mapleghost','@mapleghost','elin'),('docksparrow','@docksparrow','hiro'),('quartzlotus','@quartzlotus','cyrus'),('emberglass','@emberglass','nora'),('basinraven','@basinraven','mika'),('tideshard','@tideshard','soren'),('hollowsignal','@hollowsignal','priya'),('ironwhisper','@ironwhisper','omar'),('cinderveil','@cinderveil','rhea'),('sablekeel','@sablekeel','tara'),('lanternmoth','@lanternmoth','kian'),('frostledger','@frostledger','leena')]
7
- L=[('dockyard17','Dockyard 17'),('sector9','Sector 9'),('old_town','Old Town'),('rivergate','Rivergate'),('east_quay','East Quay'),('foundry_row','Foundry Row'),('north_basin','North Basin'),('uplink_yard','Uplink Yard')]
8
- O=[('helios_labs','Helios Labs','sector9'),('northbridge_logistics','Northbridge Logistics','dockyard17'),('apex_dynamics','Apex Dynamics','old_town'),('blueharbor_media','Blueharbor Media','old_town'),('tidewatch_ops','Tidewatch Ops','rivergate'),('kestrel_works','Kestrel Works','rivergate'),('atlas_freight','Atlas Freight','east_quay'),('sunmesh_analytics','Sunmesh Analytics','sector9'),('orion_customs','Orion Customs','north_basin'),('emberline_security','Emberline Security','foundry_row'),('harborlight_transit','Harborlight Transit','uplink_yard')]
9
- E=[('project_lantern','Project Lantern'),('black_kite','Black Kite'),('silent_current','Silent Current'),('amber_veil','Amber Veil'),('glass_harbor','Glass Harbor'),('ember_tide','Ember Tide'),('iron_wharf','Iron Wharf'),('ghost_signal','Ghost Signal')]
10
- T=[('supply_leak','supply_chain'),('port_audit','port_audit'),('customs_breach','customs_breach'),('relay_map','relay_map'),('foundry_watch','foundry_watch'),('basin_shift','basin_shift'),('quiet_manifest','quiet_manifest'),('uplink_route','uplink_route'),('ember_tide_watch','ember_tide'),('ghost_signal_net','ghost_signal')]
11
- P=['shift_roster','midnight_manifest','sat_phone_ping','drone_parts','relay_schedule','quay_ledgers','customs_tag','hull_signal','basin_photo','foundry_map','lantern_route','uplink_note']
12
-
13
- def uid(x): return f'user_{x}'
14
- def aid(x): return f'alias_{x}'
15
- def oid(x): return f'org_{x}'
16
- def lid(x): return f'loc_{x}'
17
- def eid(x): return f'event_{x}'
18
- def tid(x): return f'thr_{x}'
19
- def pid(x): return f'post_{x}'
20
-
21
- def addn(nodes,nid,nt,attrs): nodes.append({'node_id':nid,'node_type':nt,'attrs':attrs})
22
-
23
- def build():
24
- nodes=[]; edges=OrderedDict();
25
- for s,name,org,loc in U: addn(nodes,uid(s),'user',{'name':name,'org':org,'location':loc})
26
- for s,handle,user in A: addn(nodes,aid(s),'alias',{'handle':handle})
27
- for s,name,_ in O: addn(nodes,oid(s),'org',{'name':name})
28
- for s,name in L: addn(nodes,lid(s),'location',{'name':name})
29
- for s,name in E: addn(nodes,eid(s),'event',{'name':name})
30
- for s,topic in T: addn(nodes,tid(s),'thread',{'topic':topic})
31
- for s in P: addn(nodes,pid(s),'post',{'channel':'microblog'})
32
- def ae(k,src,rel,dst,c=1.0): edges[k]={'src':src,'rel':rel,'dst':dst,'confidence':c}
33
- for s,_,user in A: ae(f'a_{s}',aid(s),'alias_of',uid(user))
34
- org_map={name:oid(s) for s,name,_ in O}; loc_map={name:lid(s) for s,name in L}
35
- for s,_,org,loc in U: ae(f'w_{s}',uid(s),'works_at',org_map[org]); ae(f'l_{s}',uid(s),'located_in',loc_map[loc])
36
- for s,_,loc in O: ae(f'op_{s}',oid(s),'operates_in',lid(loc))
37
- CP=[('ivy','bharat',.95),('bharat','hiro',.95),('hiro','faris',.92),('faris','diya',.90),('diya','elin',.89),('elin','aria',.87),('aria','cyrus',.84),('cyrus','gita',.83),('gita','jules',.82),('jules','bharat',.81),('diya','ivy',.90),('ivy','elin',.86),('kian','omar',.93),('omar','mika',.90),('mika','quinn',.89),('quinn','nora',.88),('nora','rhea',.87),('rhea','soren',.86),('soren','tara',.86),('tara','kian',.84),('priya','leena',.91),('leena','aria',.83),('priya','nora',.82),('kian','bharat',.80),('soren','faris',.79),('quinn','hiro',.78)]
38
- for i,(a,b,c) in enumerate(CP,1): ae(f'c{i:02d}',uid(a),'connected_to',uid(b),c)
39
- PA={'midnight_manifest':'orchidfox','shift_roster':'docksparrow','sat_phone_ping':'nightrelay','drone_parts':'monsoonbyte','relay_schedule':'steelquill','quay_ledgers':'lanternmoth','customs_tag':'basinraven','hull_signal':'tideshard','basin_photo':'emberglass','foundry_map':'cinderveil','lantern_route':'frostledger','uplink_note':'sablekeel'}
40
- for post,author in PA.items(): ae(f'ap_{post}',aid(author),'authored_post',pid(post))
41
- PR={'midnight_manifest':['dockyard17','project_lantern'],'shift_roster':['dockyard17','northbridge_logistics'],'sat_phone_ping':['rivergate','project_lantern'],'drone_parts':['black_kite','kestrel_works'],'relay_schedule':['project_lantern','sector9'],'quay_ledgers':['east_quay','glass_harbor'],'customs_tag':['north_basin','iron_wharf'],'hull_signal':['uplink_yard','ghost_signal'],'basin_photo':['foundry_row','amber_veil'],'foundry_map':['foundry_row','ember_tide'],'lantern_route':['project_lantern','sunmesh_analytics'],'uplink_note':['uplink_yard','harborlight_transit']}
42
- for post,refs in PR.items():
43
- for i,x in enumerate(refs,1): ae(f'r_{post}_{i}',pid(post),'references', lid(x) if x in {y for y,_ in L} else (oid(x) if x in {y for y,_,_ in O} else eid(x)))
44
- TA={'supply_leak':'diya','port_audit':'jules','customs_breach':'mika','relay_map':'leena','foundry_watch':'nora','basin_shift':'quinn','quiet_manifest':'kian','uplink_route':'soren','ember_tide_watch':'rhea','ghost_signal_net':'tara'}
45
- TL={'supply_leak':[('discusses','project_lantern'),('references','northbridge_logistics')],'port_audit':[('discusses','black_kite'),('references','kestrel_works')],'customs_breach':[('discusses','iron_wharf'),('references','orion_customs')],'relay_map':[('discusses','project_lantern'),('references','sunmesh_analytics')],'foundry_watch':[('discusses','ember_tide'),('references','emberline_security')],'basin_shift':[('discusses','amber_veil'),('references','north_basin')],'quiet_manifest':[('discusses','glass_harbor'),('references','atlas_freight')],'uplink_route':[('discusses','ghost_signal'),('references','harborlight_transit')],'ember_tide_watch':[('discusses','ember_tide'),('references','foundry_row')],'ghost_signal_net':[('discusses','ghost_signal'),('references','uplink_yard')]}
46
- for t,u in TA.items(): ae(f'at_{t}',uid(u),'authored_thread',tid(t))
47
- for t,rels in TL.items():
48
- for i,(rel,x) in enumerate(rels,1): ae(f'tl_{t}_{i}',tid(t),rel, lid(x) if x in {y for y,_ in L} else (oid(x) if x in {y for y,_,_ in O} else eid(x)))
49
- ER=[('bharat','collaborates_on','project_lantern'),('hiro','collaborates_on','project_lantern'),('faris','collaborates_on','project_lantern'),('diya','investigates','project_lantern'),('leena','monitors','project_lantern'),('ivy','collaborates_on','black_kite'),('cyrus','collaborates_on','black_kite'),('elin','investigates','black_kite'),('jules','reports_on','black_kite'),('kian','collaborates_on','glass_harbor'),('omar','collaborates_on','glass_harbor'),('priya','monitors','glass_harbor'),('mika','collaborates_on','iron_wharf'),('quinn','collaborates_on','iron_wharf'),('nora','investigates','amber_veil'),('rhea','collaborates_on','ember_tide'),('soren','collaborates_on','ghost_signal'),('tara','reports_on','ghost_signal'),('gita','monitors','silent_current'),('jules','reports_on','silent_current')]
50
- for i,(u,rel,e) in enumerate(ER,1): ae(f'er{i:02d}',uid(u),rel,eid(e),.9)
51
- X=[(eid('project_lantern'),'connected_to',eid('glass_harbor')),(eid('black_kite'),'connected_to',eid('amber_veil')),(eid('ember_tide'),'connected_to',eid('ghost_signal')),(oid('atlas_freight'),'connected_to',oid('northbridge_logistics')),(oid('orion_customs'),'connected_to',oid('emberline_security')),(oid('harborlight_transit'),'connected_to',oid('tidewatch_ops'))]
52
- for i,(a,rel,b) in enumerate(X,1): ae(f'x{i:02d}',a,rel,b,.77)
53
- return nodes,edges
54
-
55
- def mk_questions(edges):
56
- def ids(*items):
57
- out=[]
58
- for it in items:
59
- if isinstance(it,list): out.extend(it)
60
- else: out.append(it)
61
- return out
62
- def rng(prefix,a,b): return [f'{prefix}{i:02d}' for i in range(a,b+1)]
63
- def sup(edge_ids): return [edges[e] for e in edge_ids]
64
- def nodes(edge_ids):
65
- s=set()
66
- for e in edge_ids: s|={edges[e]['src'],edges[e]['dst']}
67
- return len(s)
68
- qs=[]
69
- easy=[('easy_01','alias_orchidfox -> post_midnight_manifest -> loc_dockyard17 -> connected collaborator on event_project_lantern. Who is it?','user_bharat',ids('a_orchidfox','ap_midnight_manifest','r_midnight_manifest_1','c01','er01')),('easy_02','thr_supply_leak references org_northbridge_logistics. Which alias_docksparrow user works there and collaborates on event_project_lantern?','user_hiro',ids('tl_supply_leak_2','a_docksparrow','w_hiro','er02')),('easy_03','alias_monsoonbyte authored post_drone_parts about event_black_kite. Which user behind that alias is directly connected to the Kestrel collaborator?','user_diya',ids('a_monsoonbyte','ap_drone_parts','r_drone_parts_1','w_ivy','er06','c12')),('easy_04','alias_nightrelay references loc_rivergate. Which user behind it works at an org operating there and collaborates on event_project_lantern?','user_faris',ids('a_nightrelay','ap_sat_phone_ping','r_sat_phone_ping_1','w_faris','op_tidewatch_ops','er03')),('easy_05','thr_port_audit discusses Black Kite and references Kestrel Works. Which alias_orchidfox user authored post_midnight_manifest and collaborates on Black Kite?','user_ivy',ids('tl_port_audit_1','tl_port_audit_2','a_orchidfox','ap_midnight_manifest','w_ivy','er06')),('easy_06','Which Atlas Freight user behind alias_lanternmoth authored post_quay_ledgers and collaborates on event_glass_harbor?','user_kian',ids('a_lanternmoth','ap_quay_ledgers','w_kian','er10')),('easy_07','Which Orion Customs user behind alias_basinraven authored post_customs_tag and collaborates on event_iron_wharf?','user_mika',ids('a_basinraven','ap_customs_tag','w_mika','er13')),('easy_08','Which user behind alias_emberglass posted basin_photo from Foundry Row and investigates Amber Veil?','user_nora',ids('a_emberglass','ap_basin_photo','r_basin_photo_1','er15')),('easy_09','Which user behind alias_tideshard authored post_hull_signal and collaborates on Ghost Signal?','user_soren',ids('a_tideshard','ap_hull_signal','er17')),('easy_10','Which Harborlight Transit user behind alias_sablekeel authored post_uplink_note and reports on Ghost Signal?','user_tara',ids('a_sablekeel','ap_uplink_note','w_tara','er18'))]
70
- mid=[('mid_01','Follow alias_docksparrow through post_shift_roster, Dockyard 17, and the Lantern chain. Return the org node id.','org_northbridge_logistics',ids('a_docksparrow','ap_shift_roster','r_shift_roster_1','r_shift_roster_2','tl_supply_leak_2','w_hiro','l_hiro','er02','er01','c02','c03')),('mid_02','Across the Glass Harbor cluster, which user behind alias_lanternmoth links to the Atlas Freight network from thr_quiet_manifest?','user_kian',ids('a_lanternmoth','ap_quay_ledgers','r_quay_ledgers_1','r_quay_ledgers_2','at_quiet_manifest','tl_quiet_manifest_1','tl_quiet_manifest_2','w_kian','w_omar','er10','er11','er12','c13','c14')),('mid_03','Trace alias_basinraven through post_customs_tag, thr_customs_breach, and the Orion Customs collaboration chain. Who is it?','user_mika',ids('a_basinraven','ap_customs_tag','r_customs_tag_1','r_customs_tag_2','at_customs_breach','tl_customs_breach_1','tl_customs_breach_2','w_mika','w_quinn','er13','er14','c15','c16','x05')),('mid_04','In the Ember Tide and Amber Veil overlap, which Foundry Row user behind alias_cinderveil collaborates on Ember Tide?','user_rhea',ids('a_cinderveil','ap_foundry_map','r_foundry_map_1','r_foundry_map_2','at_foundry_watch','tl_foundry_watch_1','tl_foundry_watch_2','at_ember_tide_watch','tl_ember_tide_watch_1','tl_ember_tide_watch_2','w_rhea','w_nora','er15','er16','c17','x03')),('mid_05','Follow alias_tideshard from post_hull_signal into thr_uplink_route and the Harborlight relay. Return the org node id.','org_harborlight_transit',ids('a_tideshard','ap_hull_signal','r_hull_signal_1','r_hull_signal_2','at_uplink_route','tl_uplink_route_1','tl_uplink_route_2','w_soren','w_tara','er17','er18','c18','c19','op_harborlight_transit','x06')),('mid_06','Which Sunmesh user behind alias_frostledger connects post_lantern_route to thr_relay_map and the Sector 9 monitoring chain?','user_leena',ids('a_frostledger','ap_lantern_route','r_lantern_route_1','r_lantern_route_2','at_relay_map','tl_relay_map_1','tl_relay_map_2','w_leena','w_priya','l_leena','op_sunmesh_analytics','er05','c21','c22')),('mid_07','Which user behind alias_emberglass is tied to Amber Veil after combining post_basin_photo, thr_basin_shift, and the Foundry Row investigation chain?','user_nora',ids('a_emberglass','ap_basin_photo','r_basin_photo_1','r_basin_photo_2','at_basin_shift','tl_basin_shift_1','tl_basin_shift_2','w_nora','w_quinn','l_nora','er15','c16','c17','x05')),('mid_08','Combine alias_orchidfox, post_midnight_manifest, thr_supply_leak, and the Lantern to Glass Harbor bridge. Which user starts that chain?','user_ivy',ids('a_orchidfox','ap_midnight_manifest','r_midnight_manifest_1','r_midnight_manifest_2','at_supply_leak','tl_supply_leak_1','tl_supply_leak_2','w_ivy','er06','c01','c12','x01','er10','er12')),('mid_09','Which user behind alias_monsoonbyte sits at the overlap of Blueharbor Media, Project Lantern, Black Kite, and the Ivy connection chain?','user_diya',ids('a_monsoonbyte','ap_drone_parts','r_drone_parts_1','at_supply_leak','tl_supply_leak_1','at_port_audit','tl_port_audit_1','w_diya','w_ivy','w_jules','er04','er06','er09','c04','c12')),('mid_10','Who is the Northbridge user behind alias_steelquill when combining post_relay_schedule, thr_supply_leak, Dockyard 17, and Lantern collaborator edges?','user_bharat',ids('a_steelquill','ap_relay_schedule','r_relay_schedule_1','r_relay_schedule_2','at_supply_leak','tl_supply_leak_1','tl_supply_leak_2','w_bharat','w_hiro','l_bharat','l_hiro','er01','er02','c01','c02'))]
71
- big=list(edges.keys())[:58]
72
- hard=[('high_01','Lantern to Glass Harbor handoff: identify the user behind alias_orchidfox after combining Lantern logistics, Dockyard links, and Atlas Freight bridge evidence.','user_ivy',ids('a_orchidfox','ap_midnight_manifest','r_midnight_manifest_1','r_midnight_manifest_2','at_supply_leak','tl_supply_leak_1','tl_supply_leak_2',['w_ivy','w_bharat','w_hiro','w_kian','w_omar'],['l_ivy','l_bharat','l_hiro','l_kian','l_omar'],['op_northbridge_logistics','op_kestrel_works','op_atlas_freight'],rng('c',1,3),['c12','c13','c14'],['er01','er02','er03','er06','er10','er11','er12'],'at_quiet_manifest','tl_quiet_manifest_1','tl_quiet_manifest_2','ap_quay_ledgers','r_quay_ledgers_1','r_quay_ledgers_2','x01','x04','a_lanternmoth','a_steelquill','a_docksparrow')),('high_02','North Basin to Foundry Row escalation: which user behind alias_basinraven anchors the Iron Wharf side before the Emberline handoff?','user_mika',ids('a_basinraven','ap_customs_tag','r_customs_tag_1','r_customs_tag_2','at_customs_breach','tl_customs_breach_1','tl_customs_breach_2','at_basin_shift','tl_basin_shift_1','tl_basin_shift_2','at_foundry_watch','tl_foundry_watch_1','tl_foundry_watch_2',['w_mika','w_quinn','w_nora','w_rhea'],['l_mika','l_quinn','l_nora','l_rhea'],['op_orion_customs','op_emberline_security'],['c15','c16','c17'],['er13','er14','er15','er16'],'ap_basin_photo','r_basin_photo_1','r_basin_photo_2','ap_foundry_map','r_foundry_map_1','r_foundry_map_2','x02','x03','x05','a_emberglass','a_cinderveil','c23','c24')),('high_03','Harborlight ghost-signal relay: identify the user behind alias_tideshard at the Harborlight / Tidewatch junction.','user_soren',ids('a_tideshard','ap_hull_signal','r_hull_signal_1','r_hull_signal_2','a_sablekeel','ap_uplink_note','r_uplink_note_1','r_uplink_note_2','at_uplink_route','tl_uplink_route_1','tl_uplink_route_2','at_ghost_signal_net','tl_ghost_signal_net_1','tl_ghost_signal_net_2',['w_soren','w_tara','w_faris'],['l_soren','l_tara','l_faris'],['op_harborlight_transit','op_tidewatch_ops'],['c18','c19','c20','c25'],['er03','er17','er18'],'ap_sat_phone_ping','r_sat_phone_ping_1','r_sat_phone_ping_2','at_supply_leak','tl_supply_leak_1','er01','er02','x03','x06','a_nightrelay')),('high_04','Blueharbor to Black Kite to Lantern overlap: which user is the Blueharbor origin behind alias_monsoonbyte?','user_diya',ids('a_monsoonbyte','ap_drone_parts','r_drone_parts_1','r_drone_parts_2','at_port_audit','tl_port_audit_1','tl_port_audit_2','at_supply_leak','tl_supply_leak_1','tl_supply_leak_2',['w_diya','w_jules','w_ivy','w_cyrus'],['l_diya','l_jules','l_ivy','l_cyrus'],['op_blueharbor_media','op_kestrel_works','op_apex_dynamics'],['c04','c08','c09','c12'],['er04','er06','er07','er08','er09'],'a_orchidfox','ap_midnight_manifest','r_midnight_manifest_2','x01','x02','at_relay_map','tl_relay_map_1','w_leena','er05')),('high_05','Sector 9 to Dockyard 17 full relay: which user behind alias_steelquill links the Northbridge chain and the Sunmesh monitoring bridge?','user_bharat',ids('a_steelquill','ap_relay_schedule','r_relay_schedule_1','r_relay_schedule_2','a_frostledger','ap_lantern_route','r_lantern_route_1','r_lantern_route_2','at_relay_map','tl_relay_map_1','tl_relay_map_2','at_supply_leak','tl_supply_leak_1','tl_supply_leak_2',['w_bharat','w_hiro','w_leena','w_priya','w_aria'],['l_bharat','l_hiro','l_leena','l_priya','l_aria'],['op_northbridge_logistics','op_sunmesh_analytics','op_helios_labs'],['c01','c02','c05','c06','c07','c21','c22'],['er01','er02','er05'],'x01','x04','a_docksparrow','a_mapleghost','a_hollowsignal')),('high_06','Foundry Row, North Basin, and Uplink Yard spread: identify the user behind alias_emberglass before the Harborlight relay takes over.','user_nora',ids('a_emberglass','ap_basin_photo','r_basin_photo_1','r_basin_photo_2','a_cinderveil','ap_foundry_map','r_foundry_map_1','r_foundry_map_2','a_sablekeel','ap_uplink_note','r_uplink_note_1','r_uplink_note_2','at_foundry_watch','tl_foundry_watch_1','tl_foundry_watch_2','at_ember_tide_watch','tl_ember_tide_watch_1','tl_ember_tide_watch_2','at_uplink_route','tl_uplink_route_1','tl_uplink_route_2',['w_nora','w_rhea','w_soren','w_tara'],['l_nora','l_rhea','l_soren','l_tara'],['op_emberline_security','op_harborlight_transit'],['c17','c18','c19'],['er15','er16','er17','er18'],'x03','x06')),('high_07','Freight and customs bridge: which Atlas Freight user behind alias_lanternmoth connects Glass Harbor with the Northbridge chain?','user_kian',ids('a_lanternmoth','ap_quay_ledgers','r_quay_ledgers_1','r_quay_ledgers_2','at_quiet_manifest','tl_quiet_manifest_1','tl_quiet_manifest_2',['w_kian','w_omar','w_bharat','w_hiro'],['l_kian','l_omar','l_bharat','l_hiro'],['op_atlas_freight','op_northbridge_logistics'],['c13','c14','c24','c02'],['er10','er11','er12','er01','er02'],'ap_shift_roster','r_shift_roster_1','r_shift_roster_2','ap_midnight_manifest','r_midnight_manifest_1','at_supply_leak','tl_supply_leak_2','x04','a_ironwhisper','a_steelquill','a_docksparrow')),('high_08','Black Kite, Amber Veil, and Iron Wharf overlap: which user behind alias_quartzlotus is the Apex-side collaborator?','user_cyrus',ids('a_quartzlotus','w_cyrus','l_cyrus','op_apex_dynamics','er07','at_port_audit','tl_port_audit_1','ap_drone_parts','r_drone_parts_1','er15','at_basin_shift','tl_basin_shift_1','er13','at_customs_breach','tl_customs_breach_1',['w_ivy','w_nora','w_mika','w_quinn'],['l_ivy','l_nora','l_mika','l_quinn'],['op_kestrel_works','op_emberline_security','op_orion_customs'],['c08','c12','c15','c16','c17'],'x02','x05','a_orchidfox','a_basinraven','a_emberglass')),('high_09','Ghost Signal and Ember Tide relay: which user behind alias_sablekeel is the Harborlight reporting endpoint?','user_tara',ids('a_sablekeel','ap_uplink_note','r_uplink_note_1','r_uplink_note_2','a_tideshard','ap_hull_signal','r_hull_signal_1','r_hull_signal_2','at_ghost_signal_net','tl_ghost_signal_net_1','tl_ghost_signal_net_2','at_uplink_route','tl_uplink_route_1','tl_uplink_route_2','at_ember_tide_watch','tl_ember_tide_watch_1','tl_ember_tide_watch_2',['w_tara','w_soren','w_rhea','w_nora'],['l_tara','l_soren','l_rhea','l_nora'],['op_harborlight_transit','op_emberline_security'],['c18','c19','c17'],['er16','er17','er18'],'x03','x06','a_cinderveil','a_emberglass')),('high_10','End-to-end benchmark sweep: across Lantern, Black Kite, Glass Harbor, Iron Wharf, Ember Tide, and Ghost Signal, which user behind alias_hollowsignal anchors the Sunmesh monitoring side?','user_priya',big)]
73
- for diff,level,specs in [('easy',1,easy),('mid',2,mid),('high',3,hard)]:
74
- for qid,q,a,eids in specs:
75
- qs.append({'task_type':'fixed_trace','question':q,'answer':a,'supporting_edges':sup(eids),'metadata':{'difficulty':diff,'difficulty_level':level,'question_id':qid,'support_nodes':nodes(eids)}})
76
- def edge_key(e): return (e['src'], e['rel'], e['dst'])
77
- mid_pool = sup(ids('a_orchidfox','ap_midnight_manifest','r_midnight_manifest_1','r_midnight_manifest_2','a_lanternmoth','ap_quay_ledgers','r_quay_ledgers_1','r_quay_ledgers_2','a_basinraven','ap_customs_tag','r_customs_tag_1','r_customs_tag_2','a_tideshard','ap_hull_signal','r_hull_signal_1','at_supply_leak','tl_supply_leak_1','at_quiet_manifest','tl_quiet_manifest_1','er01','er02','er06','er10','c01','c02','c13'))
78
- hard_pool = sup(list(edges.keys())[:120])
79
- for q in qs:
80
- current = {edge_key(e) for e in q['supporting_edges']}
81
- diff = q['metadata']['difficulty']
82
- if diff == 'mid':
83
- pool = mid_pool
84
- target = 17
85
- elif diff == 'high':
86
- pool = hard_pool
87
- target = 50
88
- else:
89
- continue
90
- for e in pool:
91
- if q['metadata']['support_nodes'] >= target:
92
- break
93
- k = edge_key(e)
94
- if k not in current:
95
- q['supporting_edges'].append(dict(e))
96
- current.add(k)
97
- q['metadata']['support_nodes'] = len({n for edge in q['supporting_edges'] for n in (edge['src'], edge['dst'])})
98
- return qs
99
-
100
- def main():
101
- nodes,edges=build(); questions=mk_questions(edges)
102
- payload={'seeding':{'seeded_nodes':nodes,'seeded_edges':list(edges.values()),'seeded_questions':questions,'llm_generate_remaining_graph':True,'llm_generate_remaining_tasks':False,'llm_generated_edge_budget':48,'llm_generated_task_budget':0,'llm_generation_parallel':True,'llm_generation_workers':4,'llm_generation_retries':3,'allow_template_fallback_on_llm_failure':False}}
103
- out=Path('datasets/fixed_levels/seed_fixed_levels.json'); out.write_text(json.dumps(payload,indent=2),encoding='utf-8')
104
- counts=Counter(q['metadata']['difficulty'] for q in questions)
105
- stats={k:sorted(q['metadata']['support_nodes'] for q in questions if q['metadata']['difficulty']==k) for k in ['easy','mid','high']}
106
- print(json.dumps({'nodes':len(nodes),'edges':len(edges),'questions':len(questions),'difficulty_counts':dict(counts),'support_nodes':stats},indent=2))
107
-
108
- if __name__=='__main__':
109
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scratch/osint/scripts/run_openai_baseline.py DELETED
@@ -1,59 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import argparse
4
- import json
5
- import os
6
-
7
- from osint_env.baselines import OpenAIBaselineConfig, OpenAIBaselineRunner
8
-
9
-
10
- def build_parser() -> argparse.ArgumentParser:
11
- parser = argparse.ArgumentParser(description="Run the reproducible OpenAI baseline on the fixed-level OSINT benchmark.")
12
- parser.add_argument("--config", default="datasets/fixed_levels/shared_config_fixed_levels.json", help="Shared config JSON.")
13
- parser.add_argument("--seed-file", default="datasets/fixed_levels/seed_fixed_levels.json", help="Fixed seed file JSON.")
14
- parser.add_argument("--output", default="artifacts/baselines/openai_fixed_levels_latest.json", help="Baseline result JSON output path.")
15
- parser.add_argument("--leaderboard", default="artifacts/baselines/openai_fixed_levels_leaderboard.json", help="Leaderboard JSON path.")
16
- parser.add_argument("--dashboard", default="artifacts/baselines/openai_fixed_levels_dashboard.html", help="Dashboard HTML path.")
17
- parser.add_argument("--run-name", default="openai_fixed_levels_baseline", help="Leaderboard run name.")
18
- parser.add_argument("--model", default="gpt-5-nano", help="OpenAI chat model name.")
19
- parser.add_argument("--openai-base-url", default="https://api.openai.com/v1", help="OpenAI-compatible base URL.")
20
- parser.add_argument("--openai-api-key", default="", help="OpenAI API key override.")
21
- parser.add_argument("--openai-api-key-env", default="OPENAI_API_KEY", help="Environment variable name for the API key.")
22
- parser.add_argument("--episodes", type=int, default=30, help="Number of episodes to evaluate.")
23
- parser.add_argument("--max-steps", type=int, default=8, help="Episode step budget to keep runs bounded.")
24
- parser.add_argument("--temperature", type=float, default=0.0, help="Sampling temperature.")
25
- parser.add_argument("--max-tokens", type=int, default=256, help="Maximum completion tokens per step.")
26
- parser.add_argument("--timeout-seconds", type=int, default=60, help="Per-request timeout.")
27
- parser.add_argument("--seed", type=int, default=7, help="Request seed offset used for repeatable runs.")
28
- parser.add_argument("--skip-leaderboard", action="store_true", help="Do not append the run to the leaderboard file.")
29
- return parser
30
-
31
-
32
- def main() -> None:
33
- args = build_parser().parse_args()
34
- api_key = args.openai_api_key or os.getenv(args.openai_api_key_env, "")
35
- config = OpenAIBaselineConfig(
36
- shared_config_path=args.config,
37
- seed_file=args.seed_file,
38
- output_path=args.output,
39
- leaderboard_path=args.leaderboard,
40
- dashboard_path=args.dashboard,
41
- run_name=args.run_name,
42
- model=args.model,
43
- base_url=args.openai_base_url,
44
- api_key=api_key,
45
- api_key_env=args.openai_api_key_env,
46
- temperature=args.temperature,
47
- max_tokens=args.max_tokens,
48
- timeout_seconds=args.timeout_seconds,
49
- episodes=args.episodes,
50
- max_steps=args.max_steps,
51
- seed=args.seed,
52
- append_leaderboard=not args.skip_leaderboard,
53
- )
54
- result = OpenAIBaselineRunner(config).run()
55
- print(json.dumps({"summary": result["summary"], "output": args.output, "dashboard": args.dashboard}, indent=2, sort_keys=True))
56
-
57
-
58
- if __name__ == "__main__":
59
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scratch/osint/scripts/validate_release.py DELETED
@@ -1,21 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import json
4
- import sys
5
- from pathlib import Path
6
-
7
- ROOT = Path(__file__).resolve().parents[1]
8
- if str(ROOT) not in sys.path:
9
- sys.path.insert(0, str(ROOT))
10
-
11
- from osint_env.validation import run_validation_suite
12
-
13
-
14
- def main() -> int:
15
- result = run_validation_suite()
16
- print(json.dumps(result, indent=2, sort_keys=True))
17
- return 0 if result["passed"] else 1
18
-
19
-
20
- if __name__ == "__main__":
21
- raise SystemExit(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scratch/osint/server.py DELETED
@@ -1,564 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import json
4
- import os
5
- from collections import Counter
6
- from functools import lru_cache
7
- from pathlib import Path
8
- from threading import Lock
9
- from typing import Any
10
- from uuid import uuid4
11
-
12
- from fastapi import FastAPI, HTTPException, Request
13
- from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
14
-
15
- from osint_env.api import (
16
- OpenEnvActionRequest,
17
- OpenEnvInferenceReportRequest,
18
- OpenEnvInferenceReportResponse,
19
- OpenEnvObservationModel,
20
- OpenEnvResetRequest,
21
- OpenEnvResponseEnvelope,
22
- OpenEnvTaskSummary,
23
- )
24
- from osint_env.config import clone_environment_config, load_seeding_config, load_shared_config
25
- from osint_env.domain.models import Action, ActionType
26
- from osint_env.env.environment import OSINTEnvironment
27
- from osint_env.eval.leaderboard import load_leaderboard
28
- from osint_env.eval.runner import run_evaluation
29
- from osint_env.llm import build_llm_client
30
- from osint_env.viz import export_dashboard
31
-
32
-
33
- SPACE_CONFIG_PATH = Path(os.getenv("OSINT_ENV_CONFIG", "datasets/fixed_levels/shared_config_fixed_levels.json"))
34
- SPACE_SEED_PATH = Path(os.getenv("OSINT_ENV_SEED_FILE", "datasets/fixed_levels/seed_fixed_levels.json"))
35
- SPACE_PROVIDER = os.getenv("OSINT_SPACE_LLM_PROVIDER", "mock")
36
- SPACE_MODEL = os.getenv("OSINT_SPACE_LLM_MODEL", "gpt-4o-mini")
37
- SPACE_PORT = int(os.getenv("PORT", "7860"))
38
- SPACE_DASHBOARD = Path("artifacts/space_dashboard.html")
39
- LATEST_BASELINE_OUTPUT = Path("artifacts/baselines/openai_fixed_levels_latest.json")
40
- LATEST_EVALUATION_OUTPUT = Path("artifacts/latest_evaluation.json")
41
- OPENENV_SPEC_PATH = Path("openenv.yaml")
42
-
43
- _SESSION_LOCK = Lock()
44
- _SESSIONS: dict[str, OSINTEnvironment] = {}
45
- _RESET_COUNTER = 0
46
- _LATEST_SESSION_ID: str | None = None
47
-
48
-
49
- def _load_json(path: Path) -> dict[str, Any] | None:
50
- if not path.exists():
51
- return None
52
- try:
53
- payload = json.loads(path.read_text(encoding="utf-8"))
54
- except (OSError, json.JSONDecodeError):
55
- return None
56
- return payload if isinstance(payload, dict) else None
57
-
58
-
59
- def _path_mtime(path: Path) -> float:
60
- try:
61
- return path.stat().st_mtime
62
- except OSError:
63
- return 0.0
64
-
65
-
66
- def _build_environment() -> OSINTEnvironment:
67
- shared = load_shared_config(SPACE_CONFIG_PATH)
68
- env_cfg = clone_environment_config(shared.environment)
69
- if SPACE_SEED_PATH.exists():
70
- env_cfg.seeding = load_seeding_config(SPACE_SEED_PATH)
71
- env_cfg.llm.provider = SPACE_PROVIDER
72
- env_cfg.llm.model = SPACE_MODEL
73
- try:
74
- llm = build_llm_client(env_cfg.llm)
75
- except Exception:
76
- env_cfg.llm.provider = "mock"
77
- llm = build_llm_client(env_cfg.llm)
78
- return OSINTEnvironment(env_cfg, llm=llm)
79
-
80
-
81
- def _serialize_observation(observation: Any) -> OpenEnvObservationModel:
82
- return OpenEnvObservationModel(
83
- tool_outputs=list(observation.tool_outputs),
84
- graph_snapshot=dict(observation.graph_snapshot),
85
- action_history=list(observation.action_history),
86
- task=dict(observation.task),
87
- )
88
-
89
-
90
- def _safe_session_info(info: dict[str, Any]) -> dict[str, Any]:
91
- return {
92
- "step_count": int(info.get("step_count", 0)),
93
- "total_reward": float(info.get("total_reward", 0.0)),
94
- "tool_calls": int(info.get("tool_calls", 0)),
95
- "redundant_tool_calls": int(info.get("redundant_tool_calls", 0)),
96
- "task_answer": str(info.get("task_answer", "")),
97
- "agent_answer": "" if info.get("agent_answer") is None else str(info.get("agent_answer", "")),
98
- "graph_f1": float(info.get("graph_f1", 0.0)),
99
- "reward_components": dict(info.get("reward_components", {})),
100
- }
101
-
102
-
103
- def _task_summaries(env: OSINTEnvironment) -> list[OpenEnvTaskSummary]:
104
- return [
105
- OpenEnvTaskSummary(
106
- task_id=task.task_id,
107
- task_type=task.task_type,
108
- question=task.question,
109
- difficulty=str(task.metadata.get("difficulty", "unknown")),
110
- grader=(
111
- dict(task.metadata.get("grader", {}))
112
- if isinstance(task.metadata.get("grader"), dict)
113
- else {
114
- "type": "exact_match",
115
- "answer_type": "node_id",
116
- "case_sensitive": True,
117
- }
118
- ),
119
- )
120
- for task in env.tasks
121
- ]
122
-
123
-
124
- def _resolve_task_index(env: OSINTEnvironment, request: OpenEnvResetRequest) -> int:
125
- global _RESET_COUNTER
126
- if request.task_index is not None:
127
- task_index = int(request.task_index)
128
- if task_index < 0 or task_index >= len(env.tasks):
129
- raise HTTPException(status_code=400, detail=f"Invalid task_index {task_index}")
130
- return task_index
131
- if request.task_id:
132
- for idx, task in enumerate(env.tasks):
133
- if task.task_id == request.task_id:
134
- return idx
135
- raise HTTPException(status_code=400, detail=f"Unknown task_id {request.task_id}")
136
- with _SESSION_LOCK:
137
- task_index = _RESET_COUNTER % max(1, len(env.tasks))
138
- _RESET_COUNTER += 1
139
- return task_index
140
-
141
-
142
- def _get_session_env(session_id: str) -> OSINTEnvironment:
143
- with _SESSION_LOCK:
144
- env = _SESSIONS.get(session_id)
145
- if env is None:
146
- raise HTTPException(status_code=404, detail=f"Unknown session_id {session_id}")
147
- return env
148
-
149
-
150
- def _store_session(session_id: str, env: OSINTEnvironment) -> None:
151
- global _LATEST_SESSION_ID
152
- with _SESSION_LOCK:
153
- _SESSIONS[session_id] = env
154
- _LATEST_SESSION_ID = session_id
155
-
156
-
157
- def _latest_session_id() -> str:
158
- with _SESSION_LOCK:
159
- if _LATEST_SESSION_ID and _LATEST_SESSION_ID in _SESSIONS:
160
- return _LATEST_SESSION_ID
161
- if _SESSIONS:
162
- return next(reversed(_SESSIONS))
163
- raise HTTPException(status_code=404, detail="No active session. Call /reset first.")
164
-
165
-
166
- def _resolve_session_id(session_id: str | None) -> str:
167
- token = str(session_id or "").strip()
168
- if token:
169
- return token
170
- return _latest_session_id()
171
-
172
-
173
- def _task_lookup(env: OSINTEnvironment) -> dict[str, Any]:
174
- return {task.task_id: task for task in env.tasks}
175
-
176
-
177
- def _normalize_episode_rows(env: OSINTEnvironment, episodes: list[dict[str, Any]]) -> list[dict[str, Any]]:
178
- tasks_by_id = _task_lookup(env)
179
- normalized: list[dict[str, Any]] = []
180
- for episode in episodes:
181
- row = dict(episode)
182
- task = tasks_by_id.get(str(row.get("task_id", "")))
183
- if task is not None:
184
- row.setdefault("task_type", task.task_type)
185
- row.setdefault("question", task.question)
186
- row.setdefault("task_answer", task.answer)
187
- row.setdefault(
188
- "truth_edges",
189
- [
190
- {
191
- "src": edge.src,
192
- "rel": edge.rel,
193
- "dst": edge.dst,
194
- "confidence": float(edge.confidence),
195
- }
196
- for edge in task.supporting_edges
197
- ],
198
- )
199
- row.setdefault("pred_edges", [])
200
- row.setdefault("reward_components", {})
201
- row.setdefault("graph_f1", 0.0)
202
- row.setdefault("reward", 0.0)
203
- row.setdefault("steps", 0)
204
- row.setdefault("tool_calls", 0)
205
- row.setdefault("success", 0)
206
- normalized.append(row)
207
- return normalized
208
-
209
-
210
- @lru_cache(maxsize=1)
211
- def _base_environment_snapshot() -> dict[str, Any]:
212
- env = _build_environment()
213
- difficulty_counts = Counter(str(task.metadata.get("difficulty", "unknown")) for task in env.tasks)
214
- return {
215
- "task_count": len(env.tasks),
216
- "difficulty_counts": dict(difficulty_counts),
217
- "action_space": ["CALL_TOOL", "ADD_EDGE", "ANSWER"],
218
- "observation_space": {
219
- "tool_outputs": "Last tool results and memory hits.",
220
- "graph_snapshot": "Current working graph edge snapshot.",
221
- "action_history": "Recent action/reward trace.",
222
- "task": "Task id, task type, and question.",
223
- },
224
- "task_types": sorted({task.task_type for task in env.tasks}),
225
- "config": {
226
- "seed": env.config.seed,
227
- "max_steps": env.config.max_steps,
228
- "swarm_enabled": env.config.swarm.enabled,
229
- "llm_provider": env.config.llm.provider,
230
- "llm_model": env.config.llm.model,
231
- },
232
- }
233
-
234
-
235
- @lru_cache(maxsize=1)
236
- def _preview_snapshot() -> dict[str, Any]:
237
- env = _build_environment()
238
- evaluation = run_evaluation(env, episodes=3, return_details=True, llm=build_llm_client(env.config.llm))
239
- dashboard_path = export_dashboard(
240
- env=env,
241
- evaluation=evaluation,
242
- leaderboard_records=[],
243
- output_path=str(SPACE_DASHBOARD),
244
- )
245
- snapshot = dict(_base_environment_snapshot())
246
- snapshot["summary"] = evaluation["summary"]
247
- snapshot["dashboard_path"] = dashboard_path
248
- return snapshot
249
-
250
-
251
- def _space_snapshot() -> dict[str, Any]:
252
- snapshot = dict(_base_environment_snapshot())
253
-
254
- baseline_payload = _load_json(LATEST_BASELINE_OUTPUT)
255
- evaluation_payload = _load_json(LATEST_EVALUATION_OUTPUT)
256
-
257
- candidates: list[tuple[float, str, dict[str, Any]]] = []
258
- if baseline_payload is not None and isinstance(baseline_payload.get("summary"), dict):
259
- candidates.append((_path_mtime(LATEST_BASELINE_OUTPUT), "baseline_output", baseline_payload))
260
- if evaluation_payload is not None and isinstance(evaluation_payload.get("summary"), dict):
261
- candidates.append((_path_mtime(LATEST_EVALUATION_OUTPUT), "latest_evaluation", evaluation_payload))
262
-
263
- if candidates:
264
- _, source, payload = max(candidates, key=lambda item: item[0])
265
- snapshot["summary"] = dict(payload["summary"])
266
- snapshot["source"] = source
267
- if source == "baseline_output":
268
- dashboard_path = Path(
269
- str(
270
- ((payload.get("run") or {}).get("dashboard_path"))
271
- or "artifacts/baselines/openai_fixed_levels_dashboard.html"
272
- )
273
- )
274
- if dashboard_path.exists():
275
- snapshot["dashboard_path"] = str(dashboard_path)
276
- return snapshot
277
-
278
- env = _build_environment()
279
- dashboard_path = export_dashboard(
280
- env=env,
281
- evaluation=payload,
282
- leaderboard_records=[],
283
- output_path=str(SPACE_DASHBOARD),
284
- )
285
- snapshot["dashboard_path"] = dashboard_path
286
- return snapshot
287
-
288
- preview = _preview_snapshot()
289
- preview["source"] = "preview"
290
- return preview
291
-
292
-
293
- app = FastAPI(title="OSINT OpenEnv Space", version="0.1.0")
294
-
295
-
296
- @app.get("/", response_class=HTMLResponse)
297
- def home() -> str:
298
- snapshot = _space_snapshot()
299
- summary = snapshot["summary"]
300
- difficulty_html = "".join(
301
- f"<li><strong>{level}</strong>: {count}</li>"
302
- for level, count in sorted(snapshot["difficulty_counts"].items())
303
- )
304
- task_type_html = "".join(f"<li>{task_type}</li>" for task_type in snapshot["task_types"])
305
- return f"""<!doctype html>
306
- <html lang="en">
307
- <head>
308
- <meta charset="utf-8" />
309
- <meta name="viewport" content="width=device-width, initial-scale=1" />
310
- <title>OSINT OpenEnv Space</title>
311
- <style>
312
- :root {{
313
- --ink: #13212d;
314
- --muted: #4d5b69;
315
- --line: #d8e2eb;
316
- --card: #ffffff;
317
- --bg: #f6fafc;
318
- --brand: #0f766e;
319
- --accent: #b45309;
320
- }}
321
- * {{ box-sizing: border-box; }}
322
- body {{
323
- margin: 0;
324
- font-family: "Segoe UI", sans-serif;
325
- color: var(--ink);
326
- background:
327
- radial-gradient(circle at top left, rgba(15,118,110,0.12), transparent 30%),
328
- radial-gradient(circle at top right, rgba(180,83,9,0.10), transparent 28%),
329
- var(--bg);
330
- }}
331
- .wrap {{ max-width: 1120px; margin: 0 auto; padding: 24px; }}
332
- .hero, .grid {{ display: grid; gap: 16px; }}
333
- .hero {{ grid-template-columns: 1.5fr 1fr; }}
334
- .grid {{ grid-template-columns: repeat(3, minmax(0, 1fr)); margin-top: 16px; }}
335
- .card {{
336
- background: var(--card);
337
- border: 1px solid var(--line);
338
- border-radius: 18px;
339
- padding: 18px;
340
- box-shadow: 0 12px 24px rgba(19, 33, 45, 0.06);
341
- }}
342
- h1, h2 {{ margin-top: 0; }}
343
- .muted {{ color: var(--muted); }}
344
- .stats {{ display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }}
345
- .stat {{ border: 1px dashed var(--line); border-radius: 12px; padding: 10px; }}
346
- .stat .k {{ font-size: 12px; color: var(--muted); text-transform: uppercase; }}
347
- .stat .v {{ font-size: 22px; font-weight: 700; }}
348
- a.button {{
349
- display: inline-block;
350
- padding: 10px 14px;
351
- border-radius: 12px;
352
- text-decoration: none;
353
- color: white;
354
- background: var(--brand);
355
- margin-right: 10px;
356
- }}
357
- a.link {{
358
- color: var(--accent);
359
- text-decoration: none;
360
- font-weight: 600;
361
- }}
362
- ul {{ padding-left: 18px; }}
363
- code {{
364
- background: #f1f5f9;
365
- border-radius: 6px;
366
- padding: 2px 6px;
367
- }}
368
- @media (max-width: 900px) {{
369
- .hero, .grid {{ grid-template-columns: 1fr; }}
370
- }}
371
- </style>
372
- </head>
373
- <body>
374
- <div class="wrap">
375
- <div class="hero">
376
- <section class="card">
377
- <h1>OSINT OpenEnv Space</h1>
378
- <p class="muted">A containerized OpenEnv-compatible benchmark for synthetic OSINT reasoning over profiles, forum threads, posts, aliases, organizations, locations, and event links.</p>
379
- <p>The Space boots with the fixed-level benchmark so visitors get a stable environment snapshot instead of a different graph every restart.</p>
380
- <a class="button" href="/dashboard">Open Dashboard</a>
381
- <a class="link" href="/api/environment">Environment JSON</a>
382
- </section>
383
- <section class="card">
384
- <h2>Included Snapshot</h2>
385
- <div class="stats">
386
- <div class="stat"><div class="k">Tasks</div><div class="v">{snapshot["task_count"]}</div></div>
387
- <div class="stat"><div class="k">Provider</div><div class="v">{snapshot["config"]["llm_provider"]}</div></div>
388
- <div class="stat"><div class="k">Score</div><div class="v">{summary["leaderboard_score"]:.3f}</div></div>
389
- <div class="stat"><div class="k">Success</div><div class="v">{summary["task_success_rate"]:.3f}</div></div>
390
- </div>
391
- </section>
392
- </div>
393
-
394
- <div class="grid">
395
- <section class="card">
396
- <h2>Action Space</h2>
397
- <ul>
398
- <li><code>CALL_TOOL</code>: query platform views or semantic memory.</li>
399
- <li><code>ADD_EDGE</code>: add a hypothesized relation to the working graph.</li>
400
- <li><code>ANSWER</code>: submit the final node id answer.</li>
401
- </ul>
402
- </section>
403
- <section class="card">
404
- <h2>Difficulty Mix</h2>
405
- <ul>{difficulty_html}</ul>
406
- </section>
407
- <section class="card">
408
- <h2>Task Families</h2>
409
- <ul>{task_type_html}</ul>
410
- </section>
411
- </div>
412
- </div>
413
- </body>
414
- </html>"""
415
-
416
-
417
- @app.get("/healthz")
418
- def healthz() -> JSONResponse:
419
- return JSONResponse({"status": "ok"})
420
-
421
-
422
- @app.get("/health")
423
- def health() -> JSONResponse:
424
- return healthz()
425
-
426
-
427
- @app.get("/openenv.yaml")
428
- def openenv_spec() -> FileResponse:
429
- return FileResponse(OPENENV_SPEC_PATH, media_type="text/yaml")
430
-
431
-
432
- @app.get("/api/environment")
433
- def environment_metadata() -> JSONResponse:
434
- return JSONResponse(_space_snapshot())
435
-
436
-
437
- @app.get("/openenv/tasks", response_model=list[OpenEnvTaskSummary])
438
- def openenv_tasks() -> list[OpenEnvTaskSummary]:
439
- env = _build_environment()
440
- return _task_summaries(env)
441
-
442
-
443
- @app.post("/openenv/reset", response_model=OpenEnvResponseEnvelope)
444
- @app.post("/openenv/reset/", response_model=OpenEnvResponseEnvelope, include_in_schema=False)
445
- @app.post("/reset", response_model=OpenEnvResponseEnvelope, include_in_schema=False)
446
- @app.post("/reset/", response_model=OpenEnvResponseEnvelope, include_in_schema=False)
447
- async def openenv_reset(request: Request) -> OpenEnvResponseEnvelope:
448
- env = _build_environment()
449
- raw_body = await request.body()
450
- if not raw_body.strip():
451
- payload: dict[str, Any] = {}
452
- else:
453
- try:
454
- parsed_payload = json.loads(raw_body)
455
- except json.JSONDecodeError as exc:
456
- raise HTTPException(status_code=400, detail="Reset body must be valid JSON") from exc
457
- if parsed_payload is None:
458
- payload = {}
459
- elif isinstance(parsed_payload, dict):
460
- payload = parsed_payload
461
- else:
462
- raise HTTPException(status_code=400, detail="Reset body must be a JSON object")
463
-
464
- try:
465
- reset_request = OpenEnvResetRequest.model_validate(payload)
466
- except Exception as exc:
467
- raise HTTPException(status_code=422, detail="Invalid reset request payload") from exc
468
-
469
- env._task_idx = _resolve_task_index(env, reset_request)
470
- observation = env.reset()
471
- session_id = str(uuid4())
472
- _store_session(session_id, env)
473
- return OpenEnvResponseEnvelope(
474
- session_id=session_id,
475
- observation=_serialize_observation(observation),
476
- reward=0.0,
477
- done=False,
478
- info=_safe_session_info(env._info()),
479
- )
480
-
481
-
482
- @app.post("/openenv/step", response_model=OpenEnvResponseEnvelope)
483
- @app.post("/openenv/step/", response_model=OpenEnvResponseEnvelope, include_in_schema=False)
484
- @app.post("/step", response_model=OpenEnvResponseEnvelope, include_in_schema=False)
485
- @app.post("/step/", response_model=OpenEnvResponseEnvelope, include_in_schema=False)
486
- def openenv_step(request: OpenEnvActionRequest) -> OpenEnvResponseEnvelope:
487
- session_id = _resolve_session_id(request.session_id)
488
- env = _get_session_env(session_id)
489
- action_type_raw = request.resolved_action_type().strip()
490
- if not action_type_raw:
491
- raise HTTPException(status_code=400, detail="Missing action_type")
492
- try:
493
- action_type = ActionType(action_type_raw)
494
- except ValueError as exc:
495
- raise HTTPException(status_code=400, detail=f"Unsupported action_type {action_type_raw}") from exc
496
- observation, reward, done, info = env.step(Action(action_type=action_type, payload=request.resolved_payload()))
497
- return OpenEnvResponseEnvelope(
498
- session_id=session_id,
499
- observation=_serialize_observation(observation),
500
- reward=float(reward),
501
- done=bool(done),
502
- info=_safe_session_info(info),
503
- )
504
-
505
-
506
- def _state_response(session_id: str) -> OpenEnvResponseEnvelope:
507
- env = _get_session_env(session_id)
508
- if env.state is None:
509
- raise HTTPException(status_code=400, detail="Session has not been reset yet")
510
- return OpenEnvResponseEnvelope(
511
- session_id=session_id,
512
- observation=_serialize_observation(env._observation()),
513
- reward=0.0,
514
- done=bool(env.state.done),
515
- info=_safe_session_info(env._info()),
516
- )
517
-
518
-
519
- @app.get("/openenv/state/{session_id}", response_model=OpenEnvResponseEnvelope)
520
- def openenv_state(session_id: str) -> OpenEnvResponseEnvelope:
521
- return _state_response(session_id)
522
-
523
-
524
- @app.get("/openenv/state", response_model=OpenEnvResponseEnvelope, include_in_schema=False)
525
- @app.get("/state", response_model=OpenEnvResponseEnvelope, include_in_schema=False)
526
- @app.get("/state/", response_model=OpenEnvResponseEnvelope, include_in_schema=False)
527
- def openenv_state_latest() -> OpenEnvResponseEnvelope:
528
- return _state_response(_latest_session_id())
529
-
530
-
531
- @app.post("/openenv/report_inference", response_model=OpenEnvInferenceReportResponse)
532
- def openenv_report_inference(request: OpenEnvInferenceReportRequest) -> OpenEnvInferenceReportResponse:
533
- env = _build_environment()
534
- normalized_episodes = _normalize_episode_rows(env, list(request.episodes))
535
- payload = {
536
- "run": dict(request.run),
537
- "summary": dict(request.summary),
538
- "episodes": normalized_episodes,
539
- }
540
- LATEST_EVALUATION_OUTPUT.parent.mkdir(parents=True, exist_ok=True)
541
- LATEST_EVALUATION_OUTPUT.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
542
- dashboard_path = export_dashboard(
543
- env=env,
544
- evaluation=payload,
545
- leaderboard_records=load_leaderboard("artifacts/baselines/openai_fixed_levels_leaderboard.json"),
546
- output_path=str(SPACE_DASHBOARD),
547
- )
548
- return OpenEnvInferenceReportResponse(
549
- status="ok",
550
- output_path=str(LATEST_EVALUATION_OUTPUT),
551
- dashboard_path=str(dashboard_path),
552
- )
553
-
554
-
555
- @app.get("/dashboard")
556
- def dashboard() -> FileResponse:
557
- snapshot = _space_snapshot()
558
- return FileResponse(snapshot["dashboard_path"], media_type="text/html")
559
-
560
-
561
- if __name__ == "__main__":
562
- import uvicorn
563
-
564
- uvicorn.run("server:app", host="0.0.0.0", port=SPACE_PORT)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scratch/osint/server/app.py DELETED
@@ -1,26 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import importlib.util
4
- import os
5
- from pathlib import Path
6
-
7
- import uvicorn
8
-
9
-
10
- _ROOT_SERVER_PATH = Path(__file__).resolve().parents[1] / "server.py"
11
- _SPEC = importlib.util.spec_from_file_location("osint_root_server", _ROOT_SERVER_PATH)
12
- if _SPEC is None or _SPEC.loader is None:
13
- raise RuntimeError(f"Unable to load server module from {_ROOT_SERVER_PATH}")
14
-
15
- _MODULE = importlib.util.module_from_spec(_SPEC)
16
- _SPEC.loader.exec_module(_MODULE)
17
- app = _MODULE.app
18
-
19
-
20
- def main() -> None:
21
- port = int(os.getenv("PORT", "7860"))
22
- uvicorn.run("server.app:app", host="0.0.0.0", port=port)
23
-
24
-
25
- if __name__ == "__main__":
26
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scratch/osint/src/osint_env/__init__.py DELETED
@@ -1,5 +0,0 @@
1
- """OSINT RL environment package."""
2
-
3
- from .env.environment import OSINTEnvironment
4
-
5
- __all__ = ["OSINTEnvironment"]
 
 
 
 
 
 
scratch/osint/src/osint_env/agents/__init__.py DELETED
@@ -1,7 +0,0 @@
1
- """Agent implementations."""
2
-
3
- from osint_env.agents.single_agent import SingleAgentRunner
4
- from osint_env.agents.swarm_agent import SwarmAgentRunner
5
-
6
- __all__ = ["SingleAgentRunner", "SwarmAgentRunner"]
7
-
 
 
 
 
 
 
 
 
scratch/osint/src/osint_env/agents/single_agent.py DELETED
@@ -1,41 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from osint_env.domain.models import Action, ActionType
4
- from osint_env.env.environment import OSINTEnvironment
5
- from osint_env.llm.interface import LLMClient, RuleBasedMockLLM
6
-
7
-
8
- class SingleAgentRunner:
9
- def __init__(self, env: OSINTEnvironment, llm: LLMClient | None = None):
10
- self.env = env
11
- self.llm = llm or RuleBasedMockLLM()
12
-
13
- def run_episode(self) -> dict:
14
- obs = self.env.reset()
15
- done = False
16
- info = {}
17
- while not done:
18
- messages = [{"role": "system", "content": f"question: {obs.task['question']}"}]
19
- tools = []
20
- try:
21
- llm_resp = self.llm.generate(messages, tools)
22
- planned_calls = llm_resp.tool_calls[:2]
23
- except Exception:
24
- planned_calls = []
25
-
26
- for call in planned_calls:
27
- obs, _, done, info = self.env.step(Action(ActionType.CALL_TOOL, call))
28
- if done:
29
- break
30
- if done:
31
- break
32
- answer_guess = self._heuristic_answer(obs.task["question"])
33
- obs, _, done, info = self.env.step(Action(ActionType.ANSWER, {"answer": answer_guess}))
34
- return info
35
-
36
- @staticmethod
37
- def _heuristic_answer(question: str) -> str:
38
- for token in question.replace("?", "").split():
39
- if token.startswith("alias_") or token.startswith("user_"):
40
- return token
41
- return "unknown"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scratch/osint/src/osint_env/agents/swarm_agent.py DELETED
@@ -1,209 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import re
4
- from typing import Any
5
-
6
- from osint_env.domain.models import Action, ActionType
7
- from osint_env.env.environment import OSINTEnvironment
8
- from osint_env.env.spawn_reward_hooks import critical_steps, parl_style_spawn_reward
9
- from osint_env.llm.interface import LLMClient, RuleBasedMockLLM
10
-
11
-
12
- class SwarmAgentRunner:
13
- """Low-width multi-agent orchestrator over a single environment episode."""
14
-
15
- def __init__(self, env: OSINTEnvironment, llm: LLMClient | None = None):
16
- self.env = env
17
- self.llm = llm or RuleBasedMockLLM()
18
-
19
- def run_episode(self) -> dict[str, Any]:
20
- obs = self.env.reset()
21
- done = False
22
- info: dict[str, Any] = {}
23
-
24
- swarm_cfg = self.env.config.swarm
25
- spawn_cfg = self.env.config.spawn_reward
26
-
27
- spawn_count = 0
28
- finished_subtasks = 0
29
- depth_used = 0
30
- max_breadth_used = 0
31
-
32
- stage_main_steps: list[int] = []
33
- stage_sub_steps: list[list[int]] = []
34
-
35
- for _ in range(max(1, swarm_cfg.planner_rounds)):
36
- if done:
37
- break
38
-
39
- active_agents = max(1, min(swarm_cfg.max_agents, swarm_cfg.max_breadth, swarm_cfg.max_width))
40
- max_breadth_used = max(max_breadth_used, active_agents)
41
- depth_used += 1
42
- spawn_count += active_agents
43
- stage_main_steps.append(1)
44
-
45
- stage_steps: list[int] = []
46
- for agent_idx in range(active_agents):
47
- if done:
48
- break
49
-
50
- steps_for_agent = 0
51
- role = self._agent_role(agent_idx)
52
- planned_calls = self._tool_plan(
53
- obs=obs,
54
- agent_idx=agent_idx,
55
- role=role,
56
- limit=swarm_cfg.tools_per_agent,
57
- )
58
- for call in planned_calls:
59
- obs, _, done, info = self.env.step(Action(ActionType.CALL_TOOL, call))
60
- steps_for_agent += 1
61
- if done:
62
- break
63
-
64
- if not done:
65
- edge_payload = self._edge_plan(agent_idx=agent_idx)
66
- if edge_payload is not None:
67
- obs, _, done, info = self.env.step(Action(ActionType.ADD_EDGE, edge_payload))
68
- steps_for_agent += 1
69
-
70
- if steps_for_agent > 0:
71
- finished_subtasks += 1
72
- stage_steps.append(steps_for_agent)
73
-
74
- stage_sub_steps.append(stage_steps)
75
-
76
- if depth_used >= swarm_cfg.max_depth:
77
- break
78
-
79
- if not done:
80
- answer_guess = self._vote_answer()
81
- obs, _, done, info = self.env.step(Action(ActionType.ANSWER, {"answer": answer_guess}))
82
-
83
- crit_steps = critical_steps(
84
- main_steps=stage_main_steps or [1],
85
- parallel_subagent_steps=stage_sub_steps or [[]],
86
- )
87
-
88
- base_total = float(info.get("total_reward", 0.0))
89
- shaped_total = parl_style_spawn_reward(
90
- task_outcome_reward=base_total,
91
- spawn_count=spawn_count,
92
- finished_subtasks=finished_subtasks,
93
- critical_steps=max(1, crit_steps),
94
- lambda_parallel=spawn_cfg.lambda_parallel,
95
- lambda_finish=spawn_cfg.lambda_finish,
96
- anneal=spawn_cfg.anneal,
97
- breadth=max_breadth_used,
98
- depth=depth_used,
99
- max_parallel_hint=spawn_cfg.max_parallel_hint,
100
- )
101
- spawn_aux = shaped_total - base_total
102
-
103
- components = dict(info.get("reward_components", {}))
104
- components["spawn_auxiliary"] = components.get("spawn_auxiliary", 0.0) + float(spawn_aux)
105
- components["spawn_count"] = float(spawn_count)
106
- components["spawn_finished_subtasks"] = float(finished_subtasks)
107
- components["spawn_critical_steps"] = float(crit_steps)
108
- components["spawn_depth"] = float(depth_used)
109
- components["spawn_breadth"] = float(max_breadth_used)
110
-
111
- info["total_reward"] = shaped_total
112
- info["reward_components"] = components
113
- info["spawn_count"] = spawn_count
114
- info["spawn_finished_subtasks"] = finished_subtasks
115
- info["spawn_critical_steps"] = crit_steps
116
- info["spawn_depth"] = depth_used
117
- info["spawn_breadth"] = max_breadth_used
118
- info["swarm_roles"] = [self._agent_role(i) for i in range(max_breadth_used)]
119
-
120
- if self.env.state is not None:
121
- self.env.state.total_reward = shaped_total
122
- self.env.state.reward_components.update(components)
123
-
124
- return info
125
-
126
- @staticmethod
127
- def _agent_role(agent_idx: int) -> str:
128
- roles = ["explorer", "linker", "reasoner"]
129
- return roles[agent_idx % len(roles)]
130
-
131
- def _tool_plan(self, obs: Any, agent_idx: int, role: str, limit: int) -> list[dict[str, Any]]:
132
- messages = [
133
- {
134
- "role": "system",
135
- "content": (
136
- f"question: {obs.task['question']}\n"
137
- f"agent_role: {role}_{agent_idx}\n"
138
- "Return concise tool plan."
139
- ),
140
- }
141
- ]
142
- try:
143
- response = self.llm.generate(messages, tools=[])
144
- except Exception:
145
- response = None
146
-
147
- calls: list[dict[str, Any]] = []
148
- for call in (response.tool_calls if response is not None else []):
149
- if not isinstance(call, dict):
150
- continue
151
- tool_name = str(call.get("tool_name", "")).strip()
152
- args = call.get("args", {})
153
- if not tool_name or not isinstance(args, dict):
154
- continue
155
- calls.append({"tool_name": tool_name, "args": args})
156
- if len(calls) >= max(1, limit):
157
- break
158
-
159
- if calls:
160
- return calls
161
-
162
- question = str(obs.task.get("question", "")).lower()
163
- if role == "explorer":
164
- if "event" in question:
165
- return [{"tool_name": "search_threads", "args": {"topic": "security"}}]
166
- return [{"tool_name": "search_posts", "args": {"query": "Update"}}]
167
-
168
- if role == "linker":
169
- if "alias" in question:
170
- return [{"tool_name": "search_posts", "args": {"query": "alias"}}]
171
- return [{"tool_name": "search_people", "args": {"org": "Apex"}}]
172
-
173
- if role == "reasoner":
174
- return [{"tool_name": "search_memory", "args": {"query": obs.task.get("question", ""), "k": 5}}]
175
-
176
- if "alias" in question:
177
- return [{"tool_name": "search_posts", "args": {"query": "Update"}}]
178
-
179
- user_tokens = re.findall(r"\buser_[a-zA-Z0-9_]+\b", question)
180
- if user_tokens:
181
- return [{"tool_name": "get_profile", "args": {"user_id": user_tokens[0]}}]
182
-
183
- return [{"tool_name": "search_people", "args": {"org": "Apex"}}]
184
-
185
- def _edge_plan(self, agent_idx: int) -> dict[str, Any] | None:
186
- if self.env.state is None or not self.env.state.task.supporting_edges:
187
- return None
188
- edge = self.env.state.task.supporting_edges[agent_idx % len(self.env.state.task.supporting_edges)]
189
- return {
190
- "src": edge.src,
191
- "rel": edge.rel,
192
- "dst": edge.dst,
193
- "confidence": float(edge.confidence),
194
- }
195
-
196
- def _vote_answer(self) -> str:
197
- if self.env.state is None:
198
- return "unknown"
199
-
200
- truth = {(e.src, e.rel, e.dst) for e in self.env.state.task.supporting_edges}
201
- pred = {(e.src, e.rel, e.dst) for e in self.env.memory_graph.edges}
202
- if truth & pred:
203
- return self.env.state.task.answer
204
-
205
- question = self.env.state.task.question
206
- for token in question.replace("?", "").split():
207
- if token.startswith("alias_") or token.startswith("user_"):
208
- return token
209
- return "unknown"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scratch/osint/src/osint_env/api/__init__.py DELETED
@@ -1,19 +0,0 @@
1
- from osint_env.api.models import (
2
- OpenEnvActionRequest,
3
- OpenEnvInferenceReportRequest,
4
- OpenEnvInferenceReportResponse,
5
- OpenEnvObservationModel,
6
- OpenEnvResetRequest,
7
- OpenEnvResponseEnvelope,
8
- OpenEnvTaskSummary,
9
- )
10
-
11
- __all__ = [
12
- "OpenEnvActionRequest",
13
- "OpenEnvInferenceReportRequest",
14
- "OpenEnvInferenceReportResponse",
15
- "OpenEnvObservationModel",
16
- "OpenEnvResetRequest",
17
- "OpenEnvResponseEnvelope",
18
- "OpenEnvTaskSummary",
19
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scratch/osint/src/osint_env/api/models.py DELETED
@@ -1,73 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import Any
4
-
5
- from pydantic import BaseModel, Field
6
-
7
-
8
- class OpenEnvTaskSummary(BaseModel):
9
- task_id: str
10
- task_type: str
11
- question: str
12
- difficulty: str = "unknown"
13
- grader: dict[str, Any] = Field(default_factory=dict)
14
-
15
-
16
- class OpenEnvObservationModel(BaseModel):
17
- tool_outputs: list[dict[str, Any]]
18
- graph_snapshot: dict[str, Any]
19
- action_history: list[dict[str, Any]]
20
- task: dict[str, Any]
21
-
22
-
23
- class OpenEnvResetRequest(BaseModel):
24
- task_id: str | None = None
25
- task_index: int | None = None
26
-
27
-
28
- class OpenEnvActionRequest(BaseModel):
29
- session_id: str | None = Field(
30
- default=None,
31
- description="Session identifier. Optional for /step compatibility alias, which uses the latest session.",
32
- )
33
- action_type: str | None = Field(default=None, description="One of CALL_TOOL, ADD_EDGE, ANSWER.")
34
- payload: dict[str, Any] = Field(default_factory=dict)
35
- action: dict[str, Any] | None = None
36
-
37
- def resolved_action_type(self) -> str:
38
- if self.action_type:
39
- return str(self.action_type)
40
- if isinstance(self.action, dict):
41
- nested = self.action.get("action_type")
42
- if nested:
43
- return str(nested)
44
- return ""
45
-
46
- def resolved_payload(self) -> dict[str, Any]:
47
- if self.payload:
48
- return dict(self.payload)
49
- if isinstance(self.action, dict):
50
- nested = self.action.get("payload")
51
- if isinstance(nested, dict):
52
- return dict(nested)
53
- return {}
54
-
55
-
56
- class OpenEnvResponseEnvelope(BaseModel):
57
- session_id: str
58
- observation: OpenEnvObservationModel
59
- reward: float
60
- done: bool
61
- info: dict[str, Any]
62
-
63
-
64
- class OpenEnvInferenceReportRequest(BaseModel):
65
- run: dict[str, Any] = Field(default_factory=dict)
66
- summary: dict[str, Any]
67
- episodes: list[dict[str, Any]] = Field(default_factory=list)
68
-
69
-
70
- class OpenEnvInferenceReportResponse(BaseModel):
71
- status: str
72
- output_path: str
73
- dashboard_path: str
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scratch/osint/src/osint_env/baselines/__init__.py DELETED
@@ -1,4 +0,0 @@
1
- from osint_env.baselines.openai_runner import OpenAIBaselineConfig, OpenAIBaselineRunner
2
-
3
- __all__ = ["OpenAIBaselineConfig", "OpenAIBaselineRunner"]
4
-
 
 
 
 
 
scratch/osint/src/osint_env/baselines/openai_runner.py DELETED
@@ -1,533 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import json
4
- from dataclasses import asdict, dataclass
5
- from pathlib import Path
6
- from time import perf_counter
7
- from typing import Any
8
-
9
- from osint_env.config import clone_environment_config, load_seeding_config, load_shared_config
10
- from osint_env.domain.models import Action, ActionType, Edge
11
- from osint_env.env.environment import OSINTEnvironment
12
- from osint_env.env.reward import compute_graph_f1
13
- from osint_env.eval.leaderboard import append_leaderboard_record, load_leaderboard
14
- from osint_env.eval.metrics import EvalMetrics
15
- from osint_env.viz import export_dashboard
16
-
17
-
18
- SYSTEM_PROMPT = """You are an OSINT benchmark agent operating in a synthetic OpenEnv task.
19
-
20
- Available actions are provided as function tools. On every turn, call exactly one tool.
21
-
22
- Rules:
23
- - Solve the question using only tool outputs and the current graph snapshot.
24
- - When you have enough evidence, call submit_answer with the exact node id string.
25
- - Questions may contain exact node ids such as alias_*, user_*, post_*, thr_*, org_*, loc_*, and event_*.
26
- - Prefer direct id lookups when an exact id is present in the question.
27
- - get_post and get_thread retrieve exact seeded records by id.
28
- - Use add_edge only for relationships strongly supported by the evidence you have already collected.
29
- - Prefer concise, high-signal tool queries.
30
- - Never guess free-form prose when a node id answer is required.
31
- """
32
-
33
-
34
- @dataclass(slots=True)
35
- class OpenAIBaselineConfig:
36
- shared_config_path: str = "datasets/fixed_levels/shared_config_fixed_levels.json"
37
- seed_file: str = "datasets/fixed_levels/seed_fixed_levels.json"
38
- output_path: str = "artifacts/baselines/openai_fixed_levels_latest.json"
39
- leaderboard_path: str = "artifacts/baselines/openai_fixed_levels_leaderboard.json"
40
- dashboard_path: str = "artifacts/baselines/openai_fixed_levels_dashboard.html"
41
- run_name: str = "openai_fixed_levels_baseline"
42
- model: str = "gpt-5-nano"
43
- base_url: str = "https://api.openai.com/v1"
44
- api_key: str = ""
45
- api_key_env: str = "OPENAI_API_KEY"
46
- temperature: float = 0.0
47
- max_tokens: int = 256
48
- timeout_seconds: int = 60
49
- episodes: int = 30
50
- max_steps: int = 8
51
- seed: int | None = 7
52
- append_leaderboard: bool = True
53
-
54
-
55
- def _tool_schema(
56
- name: str,
57
- description: str,
58
- properties: dict[str, Any],
59
- required: list[str],
60
- ) -> dict[str, Any]:
61
- return {
62
- "type": "function",
63
- "function": {
64
- "name": name,
65
- "description": description,
66
- "parameters": {
67
- "type": "object",
68
- "properties": properties,
69
- "required": required,
70
- "additionalProperties": False,
71
- },
72
- },
73
- }
74
-
75
-
76
- def build_action_tools() -> list[dict[str, Any]]:
77
- return [
78
- _tool_schema(
79
- "search_posts",
80
- "Search microblog posts by substring over post text, post id, author id, canonical user id, or referenced entity ids/names.",
81
- {"query": {"type": "string", "description": "Substring to search for in post text."}},
82
- ["query"],
83
- ),
84
- _tool_schema(
85
- "get_post",
86
- "Fetch a specific microblog post by exact post id.",
87
- {"post_id": {"type": "string", "description": "Post node id such as post_midnight_manifest."}},
88
- ["post_id"],
89
- ),
90
- _tool_schema(
91
- "get_user_posts",
92
- "Fetch posts authored by a user or alias id. Alias ids are resolved to the canonical user and vice versa.",
93
- {"user_id": {"type": "string", "description": "User or alias node id."}},
94
- ["user_id"],
95
- ),
96
- _tool_schema(
97
- "get_mentions",
98
- "Fetch posts that mention a given canonical user id.",
99
- {"user_id": {"type": "string", "description": "Canonical user node id."}},
100
- ["user_id"],
101
- ),
102
- _tool_schema(
103
- "search_threads",
104
- "Search forum threads by exact topic name.",
105
- {"topic": {"type": "string", "description": "Thread topic such as security or ai."}},
106
- ["topic"],
107
- ),
108
- _tool_schema(
109
- "get_thread",
110
- "Fetch a specific forum thread by id.",
111
- {"thread_id": {"type": "string", "description": "Thread node id."}},
112
- ["thread_id"],
113
- ),
114
- _tool_schema(
115
- "get_user_activity",
116
- "Fetch a user's known forum activity.",
117
- {"user_id": {"type": "string", "description": "Canonical user node id."}},
118
- ["user_id"],
119
- ),
120
- _tool_schema(
121
- "get_profile",
122
- "Fetch a profile record by canonical user id or alias id.",
123
- {"user_id": {"type": "string", "description": "Canonical user node id or alias id."}},
124
- ["user_id"],
125
- ),
126
- _tool_schema(
127
- "search_people",
128
- "Search profiles by name, alias id, organization name, or organization id.",
129
- {
130
- "name": {"type": "string", "description": "Optional name substring.", "default": ""},
131
- "org": {"type": "string", "description": "Optional organization substring.", "default": ""},
132
- },
133
- [],
134
- ),
135
- _tool_schema(
136
- "get_connections",
137
- "Fetch explicit profile connections for a user or alias id.",
138
- {"user_id": {"type": "string", "description": "Canonical user node id or alias id."}},
139
- ["user_id"],
140
- ),
141
- _tool_schema(
142
- "search_memory",
143
- "Search semantic memory over prior observations and tool outputs.",
144
- {
145
- "query": {"type": "string", "description": "Memory retrieval query."},
146
- "k": {"type": "integer", "description": "Top-k matches.", "default": 5},
147
- },
148
- ["query"],
149
- ),
150
- _tool_schema(
151
- "add_edge",
152
- "Add a supported graph edge to the working memory graph.",
153
- {
154
- "src": {"type": "string"},
155
- "rel": {"type": "string"},
156
- "dst": {"type": "string"},
157
- "confidence": {"type": "number", "default": 1.0},
158
- },
159
- ["src", "rel", "dst"],
160
- ),
161
- _tool_schema(
162
- "submit_answer",
163
- "Finish the episode by submitting the exact node id answer.",
164
- {"answer": {"type": "string", "description": "Exact node id answer for the task."}},
165
- ["answer"],
166
- ),
167
- ]
168
-
169
-
170
- def _message_text(message: Any) -> str:
171
- content = getattr(message, "content", "")
172
- if isinstance(content, str):
173
- return content
174
- if isinstance(content, list):
175
- parts: list[str] = []
176
- for item in content:
177
- if isinstance(item, dict) and item.get("type") == "text":
178
- parts.append(str(item.get("text", "")))
179
- else:
180
- text = getattr(item, "text", None)
181
- if text:
182
- parts.append(str(text))
183
- return "\n".join(part for part in parts if part)
184
- return str(content or "")
185
-
186
-
187
- def _safe_info(info: dict[str, Any]) -> dict[str, Any]:
188
- return {
189
- "step_count": int(info.get("step_count", 0)),
190
- "total_reward": float(info.get("total_reward", 0.0)),
191
- "tool_calls": int(info.get("tool_calls", 0)),
192
- "redundant_tool_calls": int(info.get("redundant_tool_calls", 0)),
193
- "reward_components": dict(info.get("reward_components", {})),
194
- }
195
-
196
-
197
- def _observation_payload(env: OSINTEnvironment, observation: Any, step_limit: int) -> dict[str, Any]:
198
- task = dict(observation.task)
199
- return {
200
- "task": {
201
- "task_id": task.get("task_id", ""),
202
- "task_type": task.get("task_type", ""),
203
- "question": task.get("question", ""),
204
- },
205
- "remaining_steps": max(0, step_limit - int(env.state.step_count if env.state else 0)),
206
- "recent_tool_outputs": list(observation.tool_outputs),
207
- "graph_snapshot": dict(observation.graph_snapshot),
208
- "recent_action_history": list(observation.action_history),
209
- }
210
-
211
-
212
- class OpenAIBaselineRunner:
213
- def __init__(self, config: OpenAIBaselineConfig):
214
- self.config = config
215
-
216
- from openai import OpenAI
217
-
218
- if not config.api_key:
219
- raise ValueError(
220
- "OpenAI baseline requires an API key. "
221
- f"Set {config.api_key_env} or pass --openai-api-key."
222
- )
223
-
224
- self.client = OpenAI(
225
- api_key=config.api_key,
226
- base_url=config.base_url,
227
- timeout=config.timeout_seconds,
228
- )
229
- self.tools = build_action_tools()
230
-
231
- @staticmethod
232
- def _is_gpt5_family(model: str) -> bool:
233
- return str(model).strip().lower().startswith("gpt-5")
234
-
235
- @staticmethod
236
- def _supports_reasoning_effort_in_chat_completions(model: str) -> bool:
237
- model_name = str(model).strip().lower()
238
- if model_name.startswith("gpt-5.4-mini"):
239
- return False
240
- return model_name.startswith("gpt-5")
241
-
242
- def _request_kwargs(self, messages: list[dict[str, Any]], episode_index: int) -> dict[str, Any]:
243
- kwargs: dict[str, Any] = {
244
- "model": self.config.model,
245
- "messages": messages,
246
- "tools": self.tools,
247
- "tool_choice": "required",
248
- "parallel_tool_calls": False,
249
- "max_completion_tokens": self.config.max_tokens,
250
- }
251
- if self.config.seed is not None:
252
- kwargs["seed"] = int(self.config.seed) + episode_index
253
-
254
- if self._is_gpt5_family(self.config.model):
255
- # GPT-5 family chat-completions compatibility:
256
- # use max_completion_tokens and avoid temperature for older GPT-5 models.
257
- if self._supports_reasoning_effort_in_chat_completions(self.config.model):
258
- kwargs["reasoning_effort"] = "none"
259
- else:
260
- kwargs["temperature"] = self.config.temperature
261
-
262
- return kwargs
263
-
264
- def _build_environment(self) -> OSINTEnvironment:
265
- shared = load_shared_config(self.config.shared_config_path)
266
- env_cfg = clone_environment_config(shared.environment)
267
- env_cfg.seeding = load_seeding_config(self.config.seed_file)
268
- env_cfg.llm.provider = "mock"
269
- env_cfg.llm.model = self.config.model
270
- env_cfg.llm.temperature = self.config.temperature
271
- env_cfg.llm.max_tokens = self.config.max_tokens
272
- env_cfg.max_steps = min(int(env_cfg.max_steps), int(self.config.max_steps))
273
- return OSINTEnvironment(env_cfg)
274
-
275
- def _execute_action(
276
- self,
277
- env: OSINTEnvironment,
278
- tool_name: str,
279
- args: dict[str, Any],
280
- ) -> tuple[Any, float, bool, dict[str, Any], dict[str, Any]]:
281
- if tool_name == "submit_answer":
282
- answer = str(args.get("answer", "")).strip()
283
- obs, reward, done, info = env.step(Action(ActionType.ANSWER, {"answer": answer}))
284
- result = {"submitted_answer": answer}
285
- return obs, reward, done, info, result
286
-
287
- if tool_name == "add_edge":
288
- payload = {
289
- "src": str(args.get("src", "")).strip(),
290
- "rel": str(args.get("rel", "")).strip(),
291
- "dst": str(args.get("dst", "")).strip(),
292
- "confidence": float(args.get("confidence", 1.0)),
293
- }
294
- obs, reward, done, info = env.step(Action(ActionType.ADD_EDGE, payload))
295
- return obs, reward, done, info, payload
296
-
297
- payload = {"tool_name": tool_name, "args": dict(args)}
298
- obs, reward, done, info = env.step(Action(ActionType.CALL_TOOL, payload))
299
- result = obs.tool_outputs[-1]["output"] if obs.tool_outputs else {}
300
- return obs, reward, done, info, result
301
-
302
- def _episode(self, env: OSINTEnvironment, episode_index: int) -> tuple[dict[str, Any], dict[str, Any]]:
303
- obs = env.reset()
304
- initial_observation = _observation_payload(env, obs, env.config.max_steps)
305
- messages: list[dict[str, Any]] = [
306
- {"role": "system", "content": SYSTEM_PROMPT},
307
- {
308
- "role": "user",
309
- "content": json.dumps(initial_observation, indent=2, sort_keys=True),
310
- },
311
- ]
312
-
313
- turn_trace: list[dict[str, Any]] = []
314
- raw_fingerprints: list[str] = []
315
- info: dict[str, Any] = {}
316
- done = False
317
- usage_totals = {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}
318
-
319
- while not done and env.state is not None and env.state.step_count < env.config.max_steps:
320
- completion = self.client.chat.completions.create(**self._request_kwargs(messages, episode_index))
321
- if getattr(completion, "system_fingerprint", None):
322
- raw_fingerprints.append(str(completion.system_fingerprint))
323
- if getattr(completion, "usage", None) is not None:
324
- usage_totals["prompt_tokens"] += int(getattr(completion.usage, "prompt_tokens", 0) or 0)
325
- usage_totals["completion_tokens"] += int(getattr(completion.usage, "completion_tokens", 0) or 0)
326
- usage_totals["total_tokens"] += int(getattr(completion.usage, "total_tokens", 0) or 0)
327
-
328
- message = completion.choices[0].message
329
- content = _message_text(message)
330
- tool_calls = list(message.tool_calls or [])
331
- if not tool_calls:
332
- fallback_answer = content.strip() or "unknown"
333
- obs, reward, done, info = env.step(Action(ActionType.ANSWER, {"answer": fallback_answer}))
334
- tool_result = {
335
- "submitted_answer": fallback_answer,
336
- "reward": reward,
337
- "done": done,
338
- "observation": _observation_payload(env, obs, env.config.max_steps),
339
- "info": _safe_info(info),
340
- }
341
- messages.append({"role": "assistant", "content": content})
342
- messages.append({"role": "tool", "tool_call_id": "fallback_submit", "content": json.dumps(tool_result)})
343
- turn_trace.append(
344
- {
345
- "assistant_content": content,
346
- "tool_name": "submit_answer",
347
- "args": {"answer": fallback_answer},
348
- "tool_payload": tool_result,
349
- }
350
- )
351
- break
352
-
353
- tool_call = tool_calls[0]
354
- tool_name = str(tool_call.function.name)
355
- try:
356
- args = json.loads(tool_call.function.arguments or "{}")
357
- except json.JSONDecodeError:
358
- args = {}
359
- if not isinstance(args, dict):
360
- args = {}
361
-
362
- obs, reward, done, info, result = self._execute_action(env, tool_name, args)
363
- tool_payload = {
364
- "tool_name": tool_name,
365
- "args": args,
366
- "result": result,
367
- "reward": reward,
368
- "done": done,
369
- "observation": _observation_payload(env, obs, env.config.max_steps),
370
- "info": _safe_info(info),
371
- }
372
- assistant_message = {
373
- "role": "assistant",
374
- "content": content,
375
- "tool_calls": [
376
- {
377
- "id": tool_call.id,
378
- "type": "function",
379
- "function": {
380
- "name": tool_name,
381
- "arguments": json.dumps(args, sort_keys=True),
382
- },
383
- }
384
- ],
385
- }
386
- messages.append(assistant_message)
387
- messages.append({"role": "tool", "tool_call_id": tool_call.id, "content": json.dumps(tool_payload, sort_keys=True)})
388
- turn_trace.append(
389
- {
390
- "assistant_content": content,
391
- "tool_name": tool_name,
392
- "args": args,
393
- "reward": reward,
394
- "done": done,
395
- "tool_payload": tool_payload,
396
- }
397
- )
398
-
399
- if not done:
400
- obs, _, done, info = env.step(Action(ActionType.ANSWER, {"answer": "unknown"}))
401
- final_payload = {
402
- "submitted_answer": "unknown",
403
- "reward": 0.0,
404
- "done": done,
405
- "observation": _observation_payload(env, obs, env.config.max_steps),
406
- "info": _safe_info(info),
407
- }
408
- turn_trace.append(
409
- {
410
- "assistant_content": "",
411
- "tool_name": "submit_answer",
412
- "args": {"answer": "unknown"},
413
- "reward": 0.0,
414
- "done": done,
415
- "tool_payload": final_payload,
416
- }
417
- )
418
-
419
- info = dict(info)
420
- info["openai_system_fingerprints"] = raw_fingerprints
421
- info["usage"] = usage_totals
422
- return info, {"initial_observation": initial_observation, "turns": turn_trace}
423
-
424
- def run(self) -> dict[str, Any]:
425
- env = self._build_environment()
426
- metrics = EvalMetrics()
427
- episode_rows: list[dict[str, Any]] = []
428
-
429
- started = perf_counter()
430
- run_usage = {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}
431
- for episode_index in range(int(self.config.episodes)):
432
- info, trace = self._episode(env, episode_index)
433
- episode_usage = dict(info.get("usage", {}))
434
- for key in run_usage:
435
- run_usage[key] += int(episode_usage.get(key, 0) or 0)
436
- task_type = env.state.task.task_type if env.state else "unknown"
437
- task_id = env.state.task.task_id if env.state else f"episode_{episode_index}"
438
- truth = env.state.task.supporting_edges if env.state else []
439
- pred = env.memory_graph.edges if env.state else []
440
- graph_f1 = compute_graph_f1(pred, truth)
441
- metrics.add(info, task_type=task_type, graph_f1=graph_f1)
442
- episode_rows.append(
443
- {
444
- "task_id": task_id,
445
- "task_type": task_type,
446
- "question": env.state.task.question if env.state else "",
447
- "task_answer": str(info.get("task_answer", "")),
448
- "agent_answer": str(info.get("agent_answer", "")) if info.get("agent_answer") is not None else "",
449
- "graph_f1": graph_f1,
450
- "reward": float(info.get("total_reward", 0.0)),
451
- "steps": int(info.get("step_count", 0)),
452
- "tool_calls": int(info.get("tool_calls", 0)),
453
- "success": int(info.get("agent_answer") == info.get("task_answer")),
454
- "reward_components": dict(info.get("reward_components", {})),
455
- "pred_edges": [
456
- {
457
- "src": edge.src,
458
- "rel": edge.rel,
459
- "dst": edge.dst,
460
- "confidence": float(edge.confidence),
461
- }
462
- for edge in pred
463
- ],
464
- "truth_edges": [
465
- {
466
- "src": edge.src,
467
- "rel": edge.rel,
468
- "dst": edge.dst,
469
- "confidence": float(edge.confidence),
470
- }
471
- for edge in truth
472
- ],
473
- "trace": trace,
474
- "openai_system_fingerprints": list(info.get("openai_system_fingerprints", [])),
475
- "usage": episode_usage,
476
- }
477
- )
478
-
479
- summary = metrics.summary()
480
- duration_seconds = perf_counter() - started
481
- if self.config.append_leaderboard:
482
- record = append_leaderboard_record(
483
- path=self.config.leaderboard_path,
484
- summary=summary,
485
- episodes=int(self.config.episodes),
486
- run_name=self.config.run_name,
487
- config={
488
- "provider": "openai",
489
- "model": self.config.model,
490
- "seed": self.config.seed,
491
- "max_steps": self.config.max_steps,
492
- "shared_config_path": self.config.shared_config_path,
493
- "seed_file": self.config.seed_file,
494
- },
495
- )
496
- else:
497
- record = None
498
- dashboard_path = export_dashboard(
499
- env=env,
500
- evaluation={"summary": summary, "episodes": episode_rows},
501
- leaderboard_records=load_leaderboard(self.config.leaderboard_path),
502
- output_path=self.config.dashboard_path,
503
- )
504
-
505
- payload: dict[str, Any] = {
506
- "run": {
507
- "name": self.config.run_name,
508
- "model": self.config.model,
509
- "episodes": int(self.config.episodes),
510
- "temperature": float(self.config.temperature),
511
- "max_tokens": int(self.config.max_tokens),
512
- "timeout_seconds": int(self.config.timeout_seconds),
513
- "max_steps": int(self.config.max_steps),
514
- "seed": self.config.seed,
515
- "shared_config_path": self.config.shared_config_path,
516
- "seed_file": self.config.seed_file,
517
- "duration_seconds": duration_seconds,
518
- "dashboard_path": dashboard_path,
519
- },
520
- "summary": summary,
521
- "usage": run_usage,
522
- "episodes": episode_rows,
523
- }
524
-
525
- output = Path(self.config.output_path)
526
- output.parent.mkdir(parents=True, exist_ok=True)
527
- output.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
528
-
529
- if record is not None:
530
- payload["record"] = record
531
- output.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
532
-
533
- return payload
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scratch/osint/src/osint_env/cli.py DELETED
@@ -1,339 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import argparse
4
- import json
5
- from pathlib import Path
6
-
7
- from osint_env.agents.single_agent import SingleAgentRunner
8
- from osint_env.agents.swarm_agent import SwarmAgentRunner
9
- from osint_env.config import clone_environment_config, load_seeding_config, load_shared_config
10
- from osint_env.domain.models import EnvironmentConfig
11
- from osint_env.env.environment import OSINTEnvironment
12
- from osint_env.env.reward import compute_graph_f1
13
- from osint_env.eval.leaderboard import append_leaderboard_record, load_leaderboard, render_leaderboard_table
14
- from osint_env.eval.runner import run_evaluation
15
- from osint_env.llm import build_llm_client
16
- from osint_env.viz import export_dashboard
17
-
18
-
19
- DEFAULT_EVALUATION_PATH = "artifacts/latest_evaluation.json"
20
-
21
-
22
- def _save_evaluation(path: str, payload: dict) -> None:
23
- out = Path(path)
24
- out.parent.mkdir(parents=True, exist_ok=True)
25
- out.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
26
-
27
-
28
- def _load_evaluation(path: str) -> dict | None:
29
- file_path = Path(path)
30
- if not file_path.exists():
31
- return None
32
- try:
33
- data = json.loads(file_path.read_text(encoding="utf-8"))
34
- except json.JSONDecodeError:
35
- return None
36
- if not isinstance(data, dict):
37
- return None
38
- return data
39
-
40
-
41
- def _add_common_args(parser: argparse.ArgumentParser) -> None:
42
- parser.add_argument("--config", type=str, default="config/shared_config.json")
43
- parser.add_argument("--seed-file", type=str, default="")
44
- parser.add_argument(
45
- "--agent-mode",
46
- type=str,
47
- default="config",
48
- choices=["config", "single", "swarm"],
49
- help="Use shared config mode or override runner mode explicitly.",
50
- )
51
- parser.add_argument(
52
- "--llm-provider",
53
- type=str,
54
- default="config",
55
- choices=["config", "mock", "ollama", "openai"],
56
- help="Use shared config provider or override explicitly.",
57
- )
58
- parser.add_argument("--llm-model", type=str, default="", help="Override model name for selected LLM provider.")
59
- parser.add_argument("--llm-timeout-seconds", type=int, default=0, help="Override LLM request timeout in seconds.")
60
- parser.add_argument("--ollama-base-url", type=str, default="", help="Override Ollama base URL.")
61
- parser.add_argument("--openai-base-url", type=str, default="", help="Override OpenAI base URL.")
62
- parser.add_argument("--openai-api-key", type=str, default="", help="OpenAI API key override.")
63
- parser.add_argument(
64
- "--openai-api-key-env",
65
- type=str,
66
- default="",
67
- help="Environment variable name for OpenAI API key.",
68
- )
69
-
70
-
71
- def build_parser() -> argparse.ArgumentParser:
72
- parser = argparse.ArgumentParser(prog="osint-env")
73
- sub = parser.add_subparsers(dest="cmd", required=True)
74
-
75
- d = sub.add_parser("demo", help="Run one episode and print debug info.")
76
- _add_common_args(d)
77
-
78
- e = sub.add_parser("eval", help="Run multiple episodes and show aggregate metrics.")
79
- _add_common_args(e)
80
- e.add_argument("--episodes", type=int, default=0)
81
-
82
- b = sub.add_parser("benchmark", help="Run eval, update leaderboard, and export interactive dashboard.")
83
- _add_common_args(b)
84
- b.add_argument("--episodes", type=int, default=0)
85
- b.add_argument("--name", type=str, default="")
86
- b.add_argument("--leaderboard", type=str, default="")
87
- b.add_argument("--dashboard", type=str, default="")
88
-
89
- l = sub.add_parser("leaderboard", help="Print ranked benchmark leaderboard.")
90
- _add_common_args(l)
91
- l.add_argument("--leaderboard", type=str, default="")
92
- l.add_argument("--top", type=int, default=20)
93
- l.add_argument(
94
- "--sort-by",
95
- type=str,
96
- default="leaderboard_score",
97
- choices=[
98
- "leaderboard_score",
99
- "task_success_rate",
100
- "avg_graph_f1",
101
- "tool_efficiency",
102
- "avg_reward",
103
- "retrieval_signal",
104
- "structural_signal",
105
- "deanonymization_accuracy",
106
- "spawn_signal",
107
- ],
108
- )
109
-
110
- s = sub.add_parser("benchmark-sweep", help="Run benchmark across multiple seeds and append all runs to leaderboard.")
111
- _add_common_args(s)
112
- s.add_argument("--episodes", type=int, default=0)
113
- s.add_argument("--seeds", type=str, default="7,11,17,23,31")
114
- s.add_argument("--name-prefix", type=str, default="sweep")
115
- s.add_argument("--leaderboard", type=str, default="")
116
- s.add_argument("--dashboard-dir", type=str, default="")
117
-
118
- v = sub.add_parser("viz", help="Export an interactive graph/database explorer.")
119
- _add_common_args(v)
120
- v.add_argument("--output", type=str, default="artifacts/osint_explorer.html")
121
- v.add_argument("--with-demo", action="store_true")
122
- v.add_argument("--leaderboard", type=str, default="")
123
- v.add_argument(
124
- "--evaluation",
125
- type=str,
126
- default=DEFAULT_EVALUATION_PATH,
127
- help="Path to a saved evaluation payload with episode details.",
128
- )
129
- return parser
130
-
131
-
132
- def _resolve_environment_config(args: argparse.Namespace) -> tuple[EnvironmentConfig, dict[str, str | int]]:
133
- shared = load_shared_config(args.config)
134
- env_cfg = clone_environment_config(shared.environment)
135
-
136
- if args.seed_file:
137
- env_cfg.seeding = load_seeding_config(args.seed_file)
138
-
139
- if args.llm_provider != "config":
140
- env_cfg.llm.provider = args.llm_provider
141
- if args.llm_model:
142
- env_cfg.llm.model = args.llm_model
143
- if int(args.llm_timeout_seconds) > 0:
144
- env_cfg.llm.timeout_seconds = int(args.llm_timeout_seconds)
145
- if args.ollama_base_url:
146
- env_cfg.llm.ollama_base_url = args.ollama_base_url
147
- if args.openai_base_url:
148
- env_cfg.llm.openai_base_url = args.openai_base_url
149
- if args.openai_api_key:
150
- env_cfg.llm.openai_api_key = args.openai_api_key
151
- if args.openai_api_key_env:
152
- env_cfg.llm.openai_api_key_env = args.openai_api_key_env
153
-
154
- if args.agent_mode == "single":
155
- env_cfg.swarm.enabled = False
156
- elif args.agent_mode == "swarm":
157
- env_cfg.swarm.enabled = True
158
-
159
- runtime = {
160
- "default_episodes": shared.runtime.default_episodes,
161
- "leaderboard_path": shared.runtime.leaderboard_path,
162
- "dashboard_path": shared.runtime.dashboard_path,
163
- "sweep_dashboard_dir": shared.runtime.sweep_dashboard_dir,
164
- }
165
- return env_cfg, runtime
166
-
167
-
168
- def _runner_for(env: OSINTEnvironment) -> SingleAgentRunner | SwarmAgentRunner:
169
- if env.config.swarm.enabled:
170
- return SwarmAgentRunner(env, llm=build_llm_client(env.config.llm))
171
- return SingleAgentRunner(env, llm=build_llm_client(env.config.llm))
172
-
173
-
174
- def main() -> None:
175
- args = build_parser().parse_args()
176
- env_cfg, runtime = _resolve_environment_config(args)
177
-
178
- episodes = int(args.episodes) if getattr(args, "episodes", 0) else int(runtime["default_episodes"])
179
- leaderboard_path = str(args.leaderboard) if getattr(args, "leaderboard", "") else str(runtime["leaderboard_path"])
180
- dashboard_path = str(args.dashboard) if getattr(args, "dashboard", "") else str(runtime["dashboard_path"])
181
- sweep_dashboard_dir = (
182
- str(args.dashboard_dir) if getattr(args, "dashboard_dir", "") else str(runtime["sweep_dashboard_dir"])
183
- )
184
- evaluation_path = str(getattr(args, "evaluation", "") or DEFAULT_EVALUATION_PATH)
185
-
186
- if args.cmd == "leaderboard":
187
- records = load_leaderboard(leaderboard_path)
188
- print(render_leaderboard_table(records, top_k=args.top, sort_by=args.sort_by))
189
- return
190
-
191
- if args.cmd == "benchmark-sweep":
192
- seed_values = [int(x.strip()) for x in args.seeds.split(",") if x.strip()]
193
- outputs: list[dict[str, object]] = []
194
- for seed in seed_values:
195
- seeded_cfg = clone_environment_config(env_cfg)
196
- seeded_cfg.seed = seed
197
- env = OSINTEnvironment(seeded_cfg, llm=build_llm_client(seeded_cfg.llm))
198
- evaluation = run_evaluation(env, episodes=episodes, return_details=True, llm=build_llm_client(seeded_cfg.llm))
199
- summary = evaluation["summary"]
200
- run_name = f"{args.name_prefix}_seed{seed}"
201
- record = append_leaderboard_record(
202
- path=leaderboard_path,
203
- summary=summary,
204
- episodes=episodes,
205
- run_name=run_name,
206
- config={
207
- "seed": seed,
208
- "max_steps": env.config.max_steps,
209
- "swarm_enabled": env.config.swarm.enabled,
210
- "max_agents": env.config.swarm.max_agents,
211
- "max_breadth": env.config.swarm.max_breadth,
212
- "max_width": env.config.swarm.max_width,
213
- "max_depth": env.config.swarm.max_depth,
214
- "seeded_questions": len(env.config.seeding.seeded_questions),
215
- },
216
- )
217
- dashboard_path = export_dashboard(
218
- env=env,
219
- evaluation=evaluation,
220
- leaderboard_records=load_leaderboard(leaderboard_path),
221
- output_path=f"{sweep_dashboard_dir}/{run_name}.html",
222
- )
223
- _save_evaluation(DEFAULT_EVALUATION_PATH, evaluation)
224
- outputs.append({"seed": seed, "record": record, "dashboard": dashboard_path, "summary": summary})
225
-
226
- records = load_leaderboard(leaderboard_path)
227
- print(
228
- json.dumps(
229
- {
230
- "runs": outputs,
231
- "leaderboard_preview": render_leaderboard_table(records, top_k=min(10, len(records))),
232
- },
233
- indent=2,
234
- sort_keys=True,
235
- )
236
- )
237
- return
238
-
239
- llm_client = build_llm_client(env_cfg.llm)
240
- env = OSINTEnvironment(env_cfg, llm=llm_client)
241
- if args.cmd == "demo":
242
- info = _runner_for(env).run_episode()
243
- print(json.dumps(info, indent=2, sort_keys=True))
244
- elif args.cmd == "eval":
245
- metrics = run_evaluation(env, episodes=episodes, llm=llm_client)
246
- print(json.dumps(metrics, indent=2, sort_keys=True))
247
- elif args.cmd == "benchmark":
248
- evaluation = run_evaluation(env, episodes=episodes, return_details=True, llm=llm_client)
249
- summary = evaluation["summary"]
250
- record = append_leaderboard_record(
251
- path=leaderboard_path,
252
- summary=summary,
253
- episodes=episodes,
254
- run_name=args.name or None,
255
- config={
256
- "seed": env.config.seed,
257
- "max_steps": env.config.max_steps,
258
- "swarm_enabled": env.config.swarm.enabled,
259
- "max_agents": env.config.swarm.max_agents,
260
- "max_breadth": env.config.swarm.max_breadth,
261
- "max_width": env.config.swarm.max_width,
262
- "max_depth": env.config.swarm.max_depth,
263
- "seeded_questions": len(env.config.seeding.seeded_questions),
264
- },
265
- )
266
- leaderboard = load_leaderboard(leaderboard_path)
267
- dashboard_path = export_dashboard(
268
- env=env,
269
- evaluation=evaluation,
270
- leaderboard_records=leaderboard,
271
- output_path=dashboard_path,
272
- )
273
- _save_evaluation(DEFAULT_EVALUATION_PATH, evaluation)
274
- payload = {
275
- "record": record,
276
- "summary": summary,
277
- "dashboard": dashboard_path,
278
- }
279
- print(json.dumps(payload, indent=2, sort_keys=True))
280
- elif args.cmd == "viz":
281
- evaluation: dict | None = _load_evaluation(evaluation_path)
282
- if args.with_demo:
283
- _runner_for(env).run_episode()
284
- info = {
285
- "agent_answer": env.state.answer if env.state else "",
286
- "task_answer": env.state.task.answer if env.state else "",
287
- "total_reward": env.state.total_reward if env.state else 0.0,
288
- "step_count": env.state.step_count if env.state else 0,
289
- "tool_calls": env.state.tool_calls if env.state else 0,
290
- }
291
- evaluation = {
292
- "summary": {
293
- "task_success_rate": float(info["agent_answer"] == info["task_answer"]),
294
- "tool_efficiency": 0.0,
295
- "avg_graph_f1": 0.0,
296
- "avg_steps_to_solution": float(info["step_count"]),
297
- "deanonymization_accuracy": 0.0,
298
- "avg_reward": float(info["total_reward"]),
299
- "leaderboard_score": 0.0,
300
- },
301
- "episodes": [
302
- {
303
- "task_id": env.state.task.task_id if env.state else "n/a",
304
- "task_type": env.state.task.task_type if env.state else "n/a",
305
- "question": env.state.task.question if env.state else "n/a",
306
- "task_answer": str(info["task_answer"]),
307
- "agent_answer": str(info["agent_answer"]),
308
- "graph_f1": 0.0,
309
- "reward": float(info["total_reward"]),
310
- "steps": int(info["step_count"]),
311
- "tool_calls": int(info["tool_calls"]),
312
- "success": int(info["agent_answer"] == info["task_answer"]),
313
- }
314
- ],
315
- }
316
-
317
- graph_f1 = 0.0
318
- if env.state is not None:
319
- graph_f1 = compute_graph_f1(env.memory_graph.edges, env.state.task.supporting_edges)
320
-
321
- if evaluation is None:
322
- summary = {
323
- "task_success_rate": 0.0,
324
- "tool_efficiency": 0.0,
325
- "avg_graph_f1": graph_f1,
326
- "avg_steps_to_solution": float(env.state.step_count) if env.state else 0.0,
327
- "deanonymization_accuracy": 0.0,
328
- "avg_reward": float(env.state.total_reward) if env.state else 0.0,
329
- "leaderboard_score": 0.0,
330
- }
331
- evaluation = {"summary": summary, "episodes": []}
332
-
333
- leaderboard = load_leaderboard(leaderboard_path)
334
- out = export_dashboard(env=env, evaluation=evaluation, leaderboard_records=leaderboard, output_path=args.output)
335
- print(json.dumps({"dashboard": out, "evaluation": evaluation_path}, indent=2, sort_keys=True))
336
-
337
-
338
- if __name__ == "__main__":
339
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scratch/osint/src/osint_env/config/__init__.py DELETED
@@ -1,9 +0,0 @@
1
- from osint_env.config.shared import RuntimeDefaults, SharedConfig, clone_environment_config, load_seeding_config, load_shared_config
2
-
3
- __all__ = [
4
- "RuntimeDefaults",
5
- "SharedConfig",
6
- "clone_environment_config",
7
- "load_seeding_config",
8
- "load_shared_config",
9
- ]
 
 
 
 
 
 
 
 
 
 
scratch/osint/src/osint_env/config/shared.py DELETED
@@ -1,248 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import copy
4
- import json
5
- from dataclasses import dataclass, field
6
- from pathlib import Path
7
- from typing import Any
8
-
9
- from osint_env.domain.models import (
10
- EnvironmentConfig,
11
- LLMConfig,
12
- NodeType,
13
- SeedingConfig,
14
- SeedEdgeSpec,
15
- SeedNodeSpec,
16
- SeedQuestionSpec,
17
- SpawnRewardConfig,
18
- SwarmConfig,
19
- )
20
-
21
-
22
- @dataclass(slots=True)
23
- class RuntimeDefaults:
24
- default_episodes: int = 20
25
- leaderboard_path: str = "artifacts/leaderboard.json"
26
- dashboard_path: str = "artifacts/osint_dashboard.html"
27
- sweep_dashboard_dir: str = "artifacts/sweep_dashboards"
28
-
29
-
30
- @dataclass(slots=True)
31
- class SharedConfig:
32
- environment: EnvironmentConfig = field(default_factory=EnvironmentConfig)
33
- runtime: RuntimeDefaults = field(default_factory=RuntimeDefaults)
34
-
35
-
36
- def clone_environment_config(config: EnvironmentConfig) -> EnvironmentConfig:
37
- return copy.deepcopy(config)
38
-
39
-
40
- def _as_dict(value: Any) -> dict[str, Any]:
41
- return value if isinstance(value, dict) else {}
42
-
43
-
44
- def _parse_int(value: Any, default: int) -> int:
45
- try:
46
- return int(value)
47
- except (TypeError, ValueError):
48
- return default
49
-
50
-
51
- def _parse_float(value: Any, default: float) -> float:
52
- try:
53
- return float(value)
54
- except (TypeError, ValueError):
55
- return default
56
-
57
-
58
- def _parse_bool(value: Any, default: bool) -> bool:
59
- if isinstance(value, bool):
60
- return value
61
- if isinstance(value, str):
62
- lowered = value.strip().lower()
63
- if lowered in {"1", "true", "yes", "y", "on"}:
64
- return True
65
- if lowered in {"0", "false", "no", "n", "off"}:
66
- return False
67
- return default
68
-
69
-
70
- def _infer_node_type(node_id: str) -> NodeType:
71
- prefix = str(node_id).split("_", 1)[0].lower()
72
- mapping = {
73
- "user": NodeType.USER,
74
- "alias": NodeType.ALIAS,
75
- "org": NodeType.ORG,
76
- "loc": NodeType.LOCATION,
77
- "location": NodeType.LOCATION,
78
- "post": NodeType.POST,
79
- "thr": NodeType.THREAD,
80
- "thread": NodeType.THREAD,
81
- "event": NodeType.EVENT,
82
- }
83
- return mapping.get(prefix, NodeType.USER)
84
-
85
-
86
- def _parse_node_type(value: Any, node_id: str) -> NodeType:
87
- if isinstance(value, NodeType):
88
- return value
89
- if isinstance(value, str):
90
- raw = value.strip().lower()
91
- try:
92
- return NodeType(raw)
93
- except ValueError:
94
- return _infer_node_type(node_id)
95
- return _infer_node_type(node_id)
96
-
97
-
98
- def _parse_seed_edge(item: dict[str, Any]) -> SeedEdgeSpec | None:
99
- src = str(item.get("src", "")).strip()
100
- rel = str(item.get("rel", "")).strip()
101
- dst = str(item.get("dst", "")).strip()
102
- if not src or not rel or not dst:
103
- return None
104
- confidence = _parse_float(item.get("confidence", 1.0), 1.0)
105
- return SeedEdgeSpec(src=src, rel=rel, dst=dst, confidence=confidence)
106
-
107
-
108
- def _parse_seeding(data: dict[str, Any]) -> SeedingConfig:
109
- seeded_nodes: list[SeedNodeSpec] = []
110
- for item in data.get("seeded_nodes", []):
111
- row = _as_dict(item)
112
- node_id = str(row.get("node_id", "")).strip()
113
- if not node_id:
114
- continue
115
- node_type = _parse_node_type(row.get("node_type"), node_id)
116
- attrs = _as_dict(row.get("attrs"))
117
- seeded_nodes.append(SeedNodeSpec(node_id=node_id, node_type=node_type, attrs=attrs))
118
-
119
- seeded_edges: list[SeedEdgeSpec] = []
120
- for item in data.get("seeded_edges", []):
121
- edge = _parse_seed_edge(_as_dict(item))
122
- if edge is not None:
123
- seeded_edges.append(edge)
124
-
125
- seeded_questions: list[SeedQuestionSpec] = []
126
- for item in data.get("seeded_questions", []):
127
- row = _as_dict(item)
128
- question = str(row.get("question", "")).strip()
129
- if not question:
130
- continue
131
- answer_val = row.get("answer")
132
- answer = str(answer_val).strip() if answer_val is not None and str(answer_val).strip() else None
133
- task_type = str(row.get("task_type", "seeded")).strip() or "seeded"
134
- support_edges: list[SeedEdgeSpec] = []
135
- for edge_item in row.get("supporting_edges", []):
136
- edge = _parse_seed_edge(_as_dict(edge_item))
137
- if edge is not None:
138
- support_edges.append(edge)
139
- metadata = _as_dict(row.get("metadata"))
140
- seeded_questions.append(
141
- SeedQuestionSpec(
142
- question=question,
143
- answer=answer,
144
- task_type=task_type,
145
- supporting_edges=support_edges,
146
- metadata=metadata,
147
- )
148
- )
149
-
150
- return SeedingConfig(
151
- seeded_nodes=seeded_nodes,
152
- seeded_edges=seeded_edges,
153
- seeded_questions=seeded_questions,
154
- llm_generate_remaining_graph=_parse_bool(data.get("llm_generate_remaining_graph"), True),
155
- llm_generate_remaining_tasks=_parse_bool(data.get("llm_generate_remaining_tasks"), True),
156
- llm_generated_edge_budget=max(0, _parse_int(data.get("llm_generated_edge_budget"), 6)),
157
- llm_generated_task_budget=max(0, _parse_int(data.get("llm_generated_task_budget"), 8)),
158
- llm_generation_parallel=_parse_bool(data.get("llm_generation_parallel"), True),
159
- llm_generation_workers=max(1, _parse_int(data.get("llm_generation_workers"), 3)),
160
- llm_generation_retries=max(1, _parse_int(data.get("llm_generation_retries"), 2)),
161
- allow_template_fallback_on_llm_failure=_parse_bool(
162
- data.get("allow_template_fallback_on_llm_failure"),
163
- False,
164
- ),
165
- )
166
-
167
-
168
- def load_seeding_config(path: str | Path) -> SeedingConfig:
169
- payload = json.loads(Path(path).read_text(encoding="utf-8"))
170
- if not isinstance(payload, dict):
171
- raise ValueError("Seed file must contain a JSON object.")
172
- source = _as_dict(payload.get("seeding", payload))
173
- return _parse_seeding(source)
174
-
175
-
176
- def _parse_environment(payload: dict[str, Any]) -> EnvironmentConfig:
177
- env_data = _as_dict(payload.get("environment", payload))
178
- swarm_data = _as_dict(payload.get("swarm", env_data.get("swarm", {})))
179
- spawn_data = _as_dict(payload.get("spawn_reward", env_data.get("spawn_reward", {})))
180
- seeding_data = _as_dict(payload.get("seeding", env_data.get("seeding", {})))
181
- llm_data = _as_dict(payload.get("llm", env_data.get("llm", {})))
182
-
183
- env = EnvironmentConfig(
184
- n_users=max(4, _parse_int(env_data.get("n_users"), 40)),
185
- alias_density=max(0.0, min(1.0, _parse_float(env_data.get("alias_density"), 0.35))),
186
- noise_level=max(0.0, min(1.0, _parse_float(env_data.get("noise_level"), 0.15))),
187
- red_herring_rate=max(0.0, min(1.0, _parse_float(env_data.get("red_herring_rate"), 0.1))),
188
- max_steps=max(2, _parse_int(env_data.get("max_steps"), 18)),
189
- seed=_parse_int(env_data.get("seed"), 7),
190
- )
191
-
192
- env.swarm = SwarmConfig(
193
- enabled=_parse_bool(swarm_data.get("enabled"), False),
194
- max_agents=max(1, _parse_int(swarm_data.get("max_agents"), 3)),
195
- max_breadth=max(1, _parse_int(swarm_data.get("max_breadth"), 2)),
196
- max_width=max(1, _parse_int(swarm_data.get("max_width"), 2)),
197
- max_depth=max(1, _parse_int(swarm_data.get("max_depth"), 2)),
198
- planner_rounds=max(1, _parse_int(swarm_data.get("planner_rounds"), 2)),
199
- tools_per_agent=max(1, _parse_int(swarm_data.get("tools_per_agent"), 1)),
200
- )
201
-
202
- env.spawn_reward = SpawnRewardConfig(
203
- lambda_parallel=max(0.0, _parse_float(spawn_data.get("lambda_parallel"), 0.15)),
204
- lambda_finish=max(0.0, _parse_float(spawn_data.get("lambda_finish"), 0.2)),
205
- anneal=max(0.0, min(1.0, _parse_float(spawn_data.get("anneal"), 1.0))),
206
- max_parallel_hint=max(1, _parse_int(spawn_data.get("max_parallel_hint"), 3)),
207
- )
208
-
209
- env.seeding = _parse_seeding(seeding_data)
210
- env.llm = LLMConfig(
211
- provider=str(llm_data.get("provider", "mock")).strip() or "mock",
212
- model=str(llm_data.get("model", "qwen3:2b")).strip() or "qwen3:2b",
213
- temperature=_parse_float(llm_data.get("temperature"), 0.1),
214
- max_tokens=max(1, _parse_int(llm_data.get("max_tokens"), 256)),
215
- timeout_seconds=max(1, _parse_int(llm_data.get("timeout_seconds"), 240)),
216
- ollama_base_url=str(llm_data.get("ollama_base_url", "http://127.0.0.1:11434")).strip()
217
- or "http://127.0.0.1:11434",
218
- openai_base_url=str(llm_data.get("openai_base_url", "https://api.openai.com/v1")).strip()
219
- or "https://api.openai.com/v1",
220
- openai_api_key_env=str(llm_data.get("openai_api_key_env", "OPENAI_API_KEY")).strip() or "OPENAI_API_KEY",
221
- openai_api_key=str(llm_data.get("openai_api_key", "")).strip(),
222
- )
223
- return env
224
-
225
-
226
- def _parse_runtime(payload: dict[str, Any]) -> RuntimeDefaults:
227
- runtime = _as_dict(payload.get("runtime", {}))
228
- return RuntimeDefaults(
229
- default_episodes=max(1, _parse_int(runtime.get("default_episodes"), 20)),
230
- leaderboard_path=str(runtime.get("leaderboard_path", "artifacts/leaderboard.json")),
231
- dashboard_path=str(runtime.get("dashboard_path", "artifacts/osint_dashboard.html")),
232
- sweep_dashboard_dir=str(runtime.get("sweep_dashboard_dir", "artifacts/sweep_dashboards")),
233
- )
234
-
235
-
236
- def load_shared_config(path: str | Path | None) -> SharedConfig:
237
- if not path:
238
- return SharedConfig()
239
-
240
- file_path = Path(path)
241
- if not file_path.exists():
242
- return SharedConfig()
243
-
244
- payload = json.loads(file_path.read_text(encoding="utf-8"))
245
- if not isinstance(payload, dict):
246
- raise ValueError("Shared config file must contain a JSON object.")
247
-
248
- return SharedConfig(environment=_parse_environment(payload), runtime=_parse_runtime(payload))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scratch/osint/src/osint_env/data/__init__.py DELETED
@@ -1,2 +0,0 @@
1
- """Dataset generation package."""
2
-
 
 
 
scratch/osint/src/osint_env/data/generator.py DELETED
@@ -1,819 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from concurrent.futures import ThreadPoolExecutor, as_completed
4
- import json
5
- import random
6
- import re
7
- from dataclasses import dataclass
8
- from typing import TYPE_CHECKING, Any
9
-
10
- from osint_env.domain.models import (
11
- CanonicalGraph,
12
- Edge,
13
- EnvironmentConfig,
14
- Node,
15
- NodeType,
16
- SeedEdgeSpec,
17
- SeedQuestionSpec,
18
- TaskInstance,
19
- )
20
-
21
- if TYPE_CHECKING:
22
- from osint_env.llm.interface import LLMClient
23
-
24
-
25
- @dataclass(slots=True)
26
- class PlatformViews:
27
- microblog_posts: list[dict]
28
- forum_threads: list[dict]
29
- profiles: list[dict]
30
- alias_lookup: dict[str, str]
31
-
32
-
33
- class DatasetGenerator:
34
- def __init__(self, config: EnvironmentConfig, llm: LLMClient | None = None):
35
- self.config = config
36
- self.rng = random.Random(config.seed)
37
- self.llm = llm
38
-
39
- @staticmethod
40
- def _edge_key(edge: Edge) -> tuple[str, str, str]:
41
- return (edge.src, edge.rel, edge.dst)
42
-
43
- @staticmethod
44
- def _infer_node_type(node_id: str) -> NodeType:
45
- prefix = str(node_id).split("_", 1)[0].lower()
46
- mapping = {
47
- "user": NodeType.USER,
48
- "alias": NodeType.ALIAS,
49
- "org": NodeType.ORG,
50
- "loc": NodeType.LOCATION,
51
- "location": NodeType.LOCATION,
52
- "post": NodeType.POST,
53
- "thr": NodeType.THREAD,
54
- "thread": NodeType.THREAD,
55
- "event": NodeType.EVENT,
56
- }
57
- return mapping.get(prefix, NodeType.USER)
58
-
59
- def _ensure_node(self, graph: CanonicalGraph, node_id: str) -> None:
60
- if node_id in graph.nodes:
61
- return
62
- node_type = self._infer_node_type(node_id)
63
- attrs: dict[str, Any] = {}
64
- if node_type == NodeType.USER:
65
- attrs = {"name": node_id, "org": "Unknown", "location": "Unknown"}
66
- if node_type == NodeType.ALIAS:
67
- attrs = {"handle": f"@{node_id}"}
68
- graph.nodes[node_id] = Node(node_id=node_id, node_type=node_type, attrs=attrs)
69
-
70
- def _add_edge_if_missing(self, graph: CanonicalGraph, edge: Edge) -> None:
71
- key = self._edge_key(edge)
72
- if any(self._edge_key(existing) == key for existing in graph.edges):
73
- return
74
- self._ensure_node(graph, edge.src)
75
- self._ensure_node(graph, edge.dst)
76
- graph.edges.append(edge)
77
-
78
- @staticmethod
79
- def _extract_json_blob(text: str) -> Any:
80
- text = str(text).strip()
81
- if not text:
82
- return None
83
- for start, end in (("{", "}"), ("[", "]")):
84
- left = text.find(start)
85
- right = text.rfind(end)
86
- if left >= 0 and right > left:
87
- snippet = text[left : right + 1]
88
- try:
89
- return json.loads(snippet)
90
- except json.JSONDecodeError:
91
- continue
92
- return None
93
-
94
- def _apply_seed_nodes(self, graph: CanonicalGraph) -> None:
95
- for node_spec in self.config.seeding.seeded_nodes:
96
- node_type = (
97
- node_spec.node_type
98
- if isinstance(node_spec.node_type, NodeType)
99
- else self._infer_node_type(node_spec.node_id)
100
- )
101
- existing = graph.nodes.get(node_spec.node_id)
102
- attrs = dict(existing.attrs) if existing else {}
103
- attrs.update(node_spec.attrs)
104
- graph.nodes[node_spec.node_id] = Node(node_spec.node_id, node_type, attrs)
105
-
106
- def _apply_seed_edges(self, graph: CanonicalGraph) -> None:
107
- for edge_spec in self.config.seeding.seeded_edges:
108
- self._add_edge_if_missing(
109
- graph,
110
- Edge(
111
- src=edge_spec.src,
112
- rel=edge_spec.rel,
113
- dst=edge_spec.dst,
114
- confidence=float(edge_spec.confidence),
115
- ),
116
- )
117
-
118
- @staticmethod
119
- def _normalize_edge_candidates(value: Any) -> list[SeedEdgeSpec]:
120
- items: list[SeedEdgeSpec] = []
121
- if not isinstance(value, list):
122
- return items
123
- for row in value:
124
- if not isinstance(row, dict):
125
- continue
126
- src = str(row.get("src", "")).strip()
127
- rel = str(row.get("rel", "")).strip()
128
- dst = str(row.get("dst", "")).strip()
129
- if not src or not rel or not dst:
130
- continue
131
- try:
132
- confidence = float(row.get("confidence", 1.0))
133
- except (TypeError, ValueError):
134
- confidence = 1.0
135
- items.append(SeedEdgeSpec(src=src, rel=rel, dst=dst, confidence=confidence))
136
- return items
137
-
138
- @staticmethod
139
- def _split_budget(total: int, parts: int) -> list[int]:
140
- if total <= 0:
141
- return []
142
- slots = max(1, parts)
143
- base = total // slots
144
- remainder = total % slots
145
- chunks = [base + (1 if i < remainder else 0) for i in range(slots)]
146
- return [chunk for chunk in chunks if chunk > 0]
147
-
148
- @staticmethod
149
- def _shared_context_blob(graph: CanonicalGraph, node_limit: int = 100, edge_limit: int = 80) -> str:
150
- payload = {
151
- "known_nodes": sorted(graph.nodes.keys())[:node_limit],
152
- "known_edges": [
153
- {"src": edge.src, "rel": edge.rel, "dst": edge.dst}
154
- for edge in graph.edges[: min(edge_limit, len(graph.edges))]
155
- ],
156
- }
157
- return json.dumps(payload)
158
-
159
- def _llm_generate_json_with_retry(self, prompt: str) -> Any:
160
- if self.llm is None:
161
- return None
162
-
163
- attempts = max(1, int(self.config.seeding.llm_generation_retries))
164
- for _ in range(attempts):
165
- try:
166
- response = self.llm.generate([{"role": "system", "content": prompt}], tools=[])
167
- except Exception:
168
- continue
169
- parsed = self._extract_json_blob(response.content)
170
- if parsed is not None:
171
- return parsed
172
- return None
173
-
174
- def _run_generation_workers(self, prompts: list[str]) -> list[Any]:
175
- if not prompts:
176
- return []
177
-
178
- max_workers = max(1, min(self.config.seeding.llm_generation_workers, len(prompts)))
179
- if not self.config.seeding.llm_generation_parallel or max_workers == 1:
180
- output: list[Any] = []
181
- for prompt in prompts:
182
- parsed = self._llm_generate_json_with_retry(prompt)
183
- if parsed is not None:
184
- output.append(parsed)
185
- return output
186
-
187
- output = []
188
- with ThreadPoolExecutor(max_workers=max_workers) as executor:
189
- futures = [executor.submit(self._llm_generate_json_with_retry, prompt) for prompt in prompts]
190
- for future in as_completed(futures):
191
- try:
192
- parsed = future.result()
193
- except Exception:
194
- parsed = None
195
- if parsed is not None:
196
- output.append(parsed)
197
- return output
198
-
199
- def _template_fallback_allowed(self) -> bool:
200
- if self.llm is None:
201
- return True
202
- return bool(self.config.seeding.allow_template_fallback_on_llm_failure)
203
-
204
- def _template_generated_edges(self, graph: CanonicalGraph, budget: int) -> list[Edge]:
205
- if budget <= 0:
206
- return []
207
- users = [n.node_id for n in graph.nodes.values() if n.node_type == NodeType.USER]
208
- aliases = [n.node_id for n in graph.nodes.values() if n.node_type == NodeType.ALIAS]
209
- if len(users) < 2:
210
- return []
211
-
212
- generated: list[Edge] = []
213
- rels = ["connected_to", "mentions", "co_occurs_with"]
214
- for _ in range(budget * 3):
215
- if len(generated) >= budget:
216
- break
217
- roll = self.rng.random()
218
- if aliases and roll < 0.2:
219
- src = self.rng.choice(aliases)
220
- dst = self.rng.choice(users)
221
- rel = "alias_of"
222
- elif roll < 0.75:
223
- src, dst = self.rng.sample(users, 2)
224
- rel = self.rng.choice(rels)
225
- else:
226
- src = self.rng.choice(users)
227
- dst = self.rng.choice([u for u in users if u != src])
228
- rel = "connected_to"
229
- generated.append(Edge(src=src, rel=rel, dst=dst, confidence=0.7))
230
- return generated[:budget]
231
-
232
- def _llm_expand_graph(self, graph: CanonicalGraph, budget: int) -> list[Edge]:
233
- if budget <= 0:
234
- return []
235
-
236
- if self.llm is None:
237
- return self._template_generated_edges(graph, budget)
238
-
239
- shared_context = self._shared_context_blob(graph)
240
- workers = max(1, min(self.config.seeding.llm_generation_workers, budget))
241
- chunks = self._split_budget(budget, workers)
242
- focus_tracks = ["entity_linking", "network_expansion", "org_location", "event_trace"]
243
-
244
- prompts: list[str] = []
245
- for idx, chunk_budget in enumerate(chunks):
246
- focus = focus_tracks[idx % len(focus_tracks)]
247
- prompts.append(
248
- (
249
- "SEED_GRAPH_EXPANSION_AGENT\n"
250
- "SHARED_CONTEXT\n"
251
- f"{shared_context}\n"
252
- f"worker_id: {idx}\n"
253
- f"focus: {focus}\n"
254
- f"budget: {chunk_budget}\n"
255
- "Generate plausible graph edges for OSINT retrieval.\n"
256
- "Return STRICT JSON object: {\"edges\": [{\"src\": str, \"rel\": str, \"dst\": str, \"confidence\": float}]}.\n"
257
- "Prefer known nodes from SHARED_CONTEXT and avoid duplicates."
258
- )
259
- )
260
-
261
- generated: list[Edge] = []
262
- seen: set[tuple[str, str, str]] = set()
263
- for payload in self._run_generation_workers(prompts):
264
- raw_edges: Any = None
265
- if isinstance(payload, dict):
266
- raw_edges = payload.get("edges")
267
- elif isinstance(payload, list):
268
- raw_edges = payload
269
- for edge_spec in self._normalize_edge_candidates(raw_edges):
270
- key = (edge_spec.src, edge_spec.rel, edge_spec.dst)
271
- if key in seen:
272
- continue
273
- seen.add(key)
274
- generated.append(Edge(edge_spec.src, edge_spec.rel, edge_spec.dst, float(edge_spec.confidence)))
275
- if len(generated) >= budget:
276
- break
277
- if len(generated) >= budget:
278
- break
279
-
280
- if len(generated) < budget:
281
- residual = budget - len(generated)
282
- residual_prompt = (
283
- "SEED_GRAPH_EXPANSION_AGENT\n"
284
- "SHARED_CONTEXT\n"
285
- f"{shared_context}\n"
286
- f"budget: {residual}\n"
287
- "Generate any remaining high-utility edges.\n"
288
- "Return STRICT JSON object: {\"edges\": [{\"src\": str, \"rel\": str, \"dst\": str, \"confidence\": float}]}."
289
- )
290
- payload = self._llm_generate_json_with_retry(residual_prompt)
291
- raw_edges: Any = payload.get("edges") if isinstance(payload, dict) else payload
292
- for edge_spec in self._normalize_edge_candidates(raw_edges):
293
- key = (edge_spec.src, edge_spec.rel, edge_spec.dst)
294
- if key in seen:
295
- continue
296
- seen.add(key)
297
- generated.append(Edge(edge_spec.src, edge_spec.rel, edge_spec.dst, float(edge_spec.confidence)))
298
- if len(generated) >= budget:
299
- break
300
-
301
- if len(generated) < budget and self._template_fallback_allowed():
302
- for edge in self._template_generated_edges(graph, budget - len(generated)):
303
- key = (edge.src, edge.rel, edge.dst)
304
- if key in seen:
305
- continue
306
- seen.add(key)
307
- generated.append(edge)
308
- if len(generated) >= budget:
309
- break
310
-
311
- return generated[:budget]
312
-
313
- @staticmethod
314
- def _extract_entity_tokens(question: str) -> list[str]:
315
- return re.findall(r"\b(?:alias|user|org|loc|post|thr|thread|event)_[a-zA-Z0-9_]+\b", question)
316
-
317
- @staticmethod
318
- def _normalize_difficulty(value: str, index: int) -> str:
319
- token = str(value or "").strip().lower()
320
- if token in {"easy", "e"}:
321
- return "easy"
322
- if token in {"mid", "medium", "m"}:
323
- return "medium"
324
- if token in {"high", "hard", "h"}:
325
- return "hard"
326
- if index < 10:
327
- return "easy"
328
- if index < 20:
329
- return "medium"
330
- return "hard"
331
-
332
- @staticmethod
333
- def _task_type_for_difficulty(base_task_type: str, difficulty: str) -> str:
334
- token = str(base_task_type or "").strip().lower()
335
- if token and token != "fixed_trace":
336
- return token
337
- if difficulty == "easy":
338
- return "easy_trace"
339
- if difficulty == "medium":
340
- return "medium_trace"
341
- return "hard_trace"
342
-
343
- @staticmethod
344
- def _grader_for_difficulty(difficulty: str) -> dict[str, Any]:
345
- return {
346
- "type": "difficulty_exact_match",
347
- "answer_type": "node_id",
348
- "case_sensitive": True,
349
- "reward_profile": difficulty,
350
- "logic": {
351
- "easy": "single_agent_simplified",
352
- "medium": "reduced_components",
353
- "hard": "full_reward",
354
- }.get(difficulty, "full_reward"),
355
- }
356
-
357
- def _task_metadata(self, index: int, base_task_type: str, metadata: dict[str, Any] | None = None) -> dict[str, Any]:
358
- out = dict(metadata or {})
359
- difficulty = self._normalize_difficulty(out.get("difficulty", ""), index)
360
- out["difficulty"] = difficulty
361
- out.setdefault("grader", self._grader_for_difficulty(difficulty))
362
- out.setdefault("scenario", self._task_type_for_difficulty(base_task_type, difficulty))
363
- return out
364
-
365
- def _infer_answer_from_question(self, question: str, graph: CanonicalGraph) -> str:
366
- entities = self._extract_entity_tokens(question)
367
- question_l = question.lower()
368
-
369
- alias_tokens = [token for token in entities if token.startswith("alias_")]
370
- if alias_tokens:
371
- alias = alias_tokens[0]
372
- for edge in graph.edges:
373
- if edge.rel == "alias_of" and edge.src == alias:
374
- return edge.dst
375
-
376
- if "connected" in question_l:
377
- user_tokens = [token for token in entities if token.startswith("user_")]
378
- if user_tokens:
379
- source = user_tokens[0]
380
- for edge in graph.edges:
381
- if edge.rel == "connected_to" and edge.src == source:
382
- return edge.dst
383
-
384
- if "works at" in question_l:
385
- for edge in graph.edges:
386
- if edge.rel != "works_at":
387
- continue
388
- org = graph.nodes.get(edge.dst)
389
- org_name = str((org.attrs or {}).get("name", "")).lower() if org else ""
390
- if org_name and org_name in question_l:
391
- return edge.src
392
-
393
- return entities[0] if entities else "unknown"
394
-
395
- def _infer_support_edges(self, question: str, answer: str, graph: CanonicalGraph) -> list[Edge]:
396
- if answer:
397
- for edge in graph.edges:
398
- if edge.dst == answer or edge.src == answer:
399
- if edge.src in question or edge.dst in question or edge.rel in question.lower():
400
- return [edge]
401
-
402
- entities = self._extract_entity_tokens(question)
403
- for edge in graph.edges:
404
- if edge.src in entities or edge.dst in entities:
405
- return [edge]
406
- return []
407
-
408
- def _seeded_tasks(self, graph: CanonicalGraph) -> list[TaskInstance]:
409
- tasks: list[TaskInstance] = []
410
- for idx, question_spec in enumerate(self.config.seeding.seeded_questions):
411
- answer = question_spec.answer or self._infer_answer_from_question(question_spec.question, graph)
412
- metadata = self._task_metadata(idx, question_spec.task_type, dict(question_spec.metadata))
413
- difficulty = str(metadata.get("difficulty", "hard"))
414
- if question_spec.supporting_edges:
415
- support = [
416
- Edge(src=e.src, rel=e.rel, dst=e.dst, confidence=float(e.confidence))
417
- for e in question_spec.supporting_edges
418
- ]
419
- else:
420
- support = self._infer_support_edges(question_spec.question, answer, graph)
421
-
422
- tasks.append(
423
- TaskInstance(
424
- task_id=f"seed_task_{idx}",
425
- task_type=self._task_type_for_difficulty(question_spec.task_type, difficulty),
426
- question=question_spec.question,
427
- answer=answer,
428
- supporting_edges=support,
429
- metadata=metadata,
430
- )
431
- )
432
- return tasks
433
-
434
- def _template_tasks(self, graph: CanonicalGraph, count: int, start_idx: int = 0) -> list[TaskInstance]:
435
- alias_edges = [e for e in graph.edges if e.rel == "alias_of"]
436
- conn_edges = [e for e in graph.edges if e.rel == "connected_to"]
437
- work_edges = [e for e in graph.edges if e.rel == "works_at"]
438
- tasks: list[TaskInstance] = []
439
-
440
- for i in range(count):
441
- mode = self.rng.choice(["identity_resolution", "network_discovery", "event_tracing"])
442
- if mode == "identity_resolution" and alias_edges:
443
- edge = self.rng.choice(alias_edges)
444
- q = f"Which canonical user owns alias {edge.src}?"
445
- a = edge.dst
446
- support = [edge]
447
- elif mode == "network_discovery" and conn_edges:
448
- edge = self.rng.choice(conn_edges)
449
- q = f"Who is connected to {edge.src}?"
450
- a = edge.dst
451
- support = [edge]
452
- else:
453
- edge = self.rng.choice(work_edges)
454
- org_node = graph.nodes.get(edge.dst)
455
- org_name = (org_node.attrs or {}).get("name", edge.dst) if org_node else edge.dst
456
- q = f"Which user works at {org_name}?"
457
- a = edge.src
458
- support = [edge]
459
- tasks.append(
460
- TaskInstance(
461
- task_id=f"task_{start_idx + i}",
462
- task_type=mode,
463
- question=q,
464
- answer=a,
465
- supporting_edges=support,
466
- metadata=self._task_metadata(start_idx + i, mode),
467
- )
468
- )
469
- return tasks
470
-
471
- def _llm_generated_tasks(self, graph: CanonicalGraph, count: int, start_idx: int) -> list[TaskInstance]:
472
- if count <= 0:
473
- return []
474
- if self.llm is None:
475
- return self._template_tasks(graph, count=count, start_idx=start_idx)
476
-
477
- candidate_edges = [
478
- {"src": edge.src, "rel": edge.rel, "dst": edge.dst}
479
- for edge in graph.edges
480
- if edge.rel in {"alias_of", "connected_to", "works_at"}
481
- ][:60]
482
- shared_context = json.dumps(
483
- {
484
- "known_nodes": sorted(graph.nodes.keys())[:100],
485
- "edge_sample": candidate_edges,
486
- }
487
- )
488
- workers = max(1, min(self.config.seeding.llm_generation_workers, count))
489
- chunks = self._split_budget(count, workers)
490
- focus_tracks = ["identity_resolution", "network_discovery", "event_tracing", "deanonymization"]
491
-
492
- prompts: list[str] = []
493
- for idx, chunk_budget in enumerate(chunks):
494
- focus = focus_tracks[idx % len(focus_tracks)]
495
- prompts.append(
496
- (
497
- "SEED_TASK_EXPANSION_AGENT\n"
498
- "SHARED_CONTEXT\n"
499
- f"{shared_context}\n"
500
- f"worker_id: {idx}\n"
501
- f"focus: {focus}\n"
502
- f"task_budget: {chunk_budget}\n"
503
- "Generate OSINT QA tasks with answers and support edges.\n"
504
- "Return STRICT JSON object: {\"tasks\": [{\"task_type\": str, \"question\": str, \"answer\": str, \"supporting_edges\": [{\"src\": str, \"rel\": str, \"dst\": str, \"confidence\": float}]}]}."
505
- )
506
- )
507
-
508
- llm_tasks: list[TaskInstance] = []
509
- seen_questions: set[str] = set()
510
- for payload in self._run_generation_workers(prompts):
511
- raw_tasks: Any = None
512
- if isinstance(payload, dict):
513
- raw_tasks = payload.get("tasks")
514
- elif isinstance(payload, list):
515
- raw_tasks = payload
516
- if not isinstance(raw_tasks, list):
517
- continue
518
-
519
- for row in raw_tasks:
520
- if not isinstance(row, dict):
521
- continue
522
- question = str(row.get("question", "")).strip()
523
- if not question:
524
- continue
525
- key = question.lower()
526
- if key in seen_questions:
527
- continue
528
- seen_questions.add(key)
529
- answer = str(row.get("answer", "")).strip() or self._infer_answer_from_question(question, graph)
530
- task_type = str(row.get("task_type", "llm_generated")).strip() or "llm_generated"
531
- support_specs = self._normalize_edge_candidates(row.get("supporting_edges"))
532
- if support_specs:
533
- support = [Edge(e.src, e.rel, e.dst, e.confidence) for e in support_specs]
534
- else:
535
- support = self._infer_support_edges(question, answer, graph)
536
- llm_tasks.append(
537
- TaskInstance(
538
- task_id=f"task_{start_idx + len(llm_tasks)}",
539
- task_type=task_type,
540
- question=question,
541
- answer=answer,
542
- supporting_edges=support,
543
- metadata=self._task_metadata(
544
- start_idx + len(llm_tasks),
545
- task_type,
546
- {"generated_by": "llm", "shared_context": True},
547
- ),
548
- )
549
- )
550
- if len(llm_tasks) >= count:
551
- break
552
- if len(llm_tasks) >= count:
553
- break
554
-
555
- if len(llm_tasks) < count:
556
- residual = count - len(llm_tasks)
557
- residual_prompt = (
558
- "SEED_TASK_EXPANSION_AGENT\n"
559
- "SHARED_CONTEXT\n"
560
- f"{shared_context}\n"
561
- f"task_budget: {residual}\n"
562
- "Generate additional tasks not already present in SHARED_CONTEXT.\n"
563
- "Return STRICT JSON object: {\"tasks\": [{\"task_type\": str, \"question\": str, \"answer\": str, \"supporting_edges\": [{\"src\": str, \"rel\": str, \"dst\": str, \"confidence\": float}]}]}."
564
- )
565
- payload = self._llm_generate_json_with_retry(residual_prompt)
566
- raw_tasks: Any = payload.get("tasks") if isinstance(payload, dict) else payload
567
- if isinstance(raw_tasks, list):
568
- for row in raw_tasks:
569
- if not isinstance(row, dict):
570
- continue
571
- question = str(row.get("question", "")).strip()
572
- if not question:
573
- continue
574
- key = question.lower()
575
- if key in seen_questions:
576
- continue
577
- seen_questions.add(key)
578
- answer = str(row.get("answer", "")).strip() or self._infer_answer_from_question(question, graph)
579
- task_type = str(row.get("task_type", "llm_generated")).strip() or "llm_generated"
580
- support_specs = self._normalize_edge_candidates(row.get("supporting_edges"))
581
- if support_specs:
582
- support = [Edge(e.src, e.rel, e.dst, e.confidence) for e in support_specs]
583
- else:
584
- support = self._infer_support_edges(question, answer, graph)
585
- llm_tasks.append(
586
- TaskInstance(
587
- task_id=f"task_{start_idx + len(llm_tasks)}",
588
- task_type=task_type,
589
- question=question,
590
- answer=answer,
591
- supporting_edges=support,
592
- metadata=self._task_metadata(
593
- start_idx + len(llm_tasks),
594
- task_type,
595
- {"generated_by": "llm", "shared_context": True},
596
- ),
597
- )
598
- )
599
- if len(llm_tasks) >= count:
600
- break
601
-
602
- if len(llm_tasks) < count and self._template_fallback_allowed():
603
- llm_tasks.extend(
604
- self._template_tasks(
605
- graph,
606
- count=count - len(llm_tasks),
607
- start_idx=start_idx + len(llm_tasks),
608
- )
609
- )
610
- return llm_tasks[:count]
611
-
612
- def build_canonical_graph(self) -> CanonicalGraph:
613
- graph = CanonicalGraph()
614
- orgs = ["Apex Dynamics", "Helios Labs", "Northbridge"]
615
- locations = ["Bengaluru", "Pune", "Hyderabad", "Delhi"]
616
-
617
- for i in range(self.config.n_users):
618
- uid = f"user_{i}"
619
- org = self.rng.choice(orgs)
620
- loc = self.rng.choice(locations)
621
- graph.nodes[uid] = Node(uid, NodeType.USER, {"name": f"Person {i}", "org": org, "location": loc})
622
- org_id = f"org_{org.lower().replace(' ', '_')}"
623
- loc_id = f"loc_{loc.lower()}"
624
- graph.nodes.setdefault(org_id, Node(org_id, NodeType.ORG, {"name": org}))
625
- graph.nodes.setdefault(loc_id, Node(loc_id, NodeType.LOCATION, {"name": loc}))
626
- graph.edges.append(Edge(uid, "works_at", org_id))
627
- graph.edges.append(Edge(uid, "located_in", loc_id))
628
-
629
- if self.rng.random() < self.config.alias_density:
630
- alias = f"alias_{i}_{self.rng.randint(100,999)}"
631
- graph.nodes[alias] = Node(alias, NodeType.ALIAS, {"handle": f"@{alias}"})
632
- graph.edges.append(Edge(alias, "alias_of", uid))
633
-
634
- users = [n for n in graph.nodes.values() if n.node_type == NodeType.USER]
635
- for _ in range(max(1, self.config.n_users // 2)):
636
- a, b = self.rng.sample(users, 2)
637
- graph.edges.append(Edge(a.node_id, "connected_to", b.node_id, confidence=0.8))
638
-
639
- self._apply_seed_nodes(graph)
640
- self._apply_seed_edges(graph)
641
-
642
- if self.config.seeding.llm_generate_remaining_graph:
643
- llm_edges = self._llm_expand_graph(graph, self.config.seeding.llm_generated_edge_budget)
644
- for edge in llm_edges:
645
- self._add_edge_if_missing(graph, edge)
646
- return graph
647
-
648
- def build_platform_views(self, graph: CanonicalGraph) -> PlatformViews:
649
- users = [n for n in graph.nodes.values() if n.node_type == NodeType.USER]
650
- aliases = [n for n in graph.nodes.values() if n.node_type == NodeType.ALIAS]
651
- alias_owner = {e.src: e.dst for e in graph.edges if e.rel == "alias_of"}
652
- user_aliases: dict[str, list[str]] = {}
653
- for alias_id, user_id in alias_owner.items():
654
- user_aliases.setdefault(user_id, []).append(alias_id)
655
- node_names = {
656
- node_id: str((node.attrs or {}).get("name") or (node.attrs or {}).get("handle") or node_id)
657
- for node_id, node in graph.nodes.items()
658
- }
659
-
660
- microblog_posts: list[dict] = []
661
- for i, user in enumerate(users):
662
- poster = user.node_id
663
- if aliases and self.rng.random() < 0.45:
664
- candidate = self.rng.choice(aliases).node_id
665
- poster = candidate
666
- text = f"Update {i} from {user.attrs['org']} #{user.attrs['location'].lower()}"
667
- if self.rng.random() < self.config.noise_level:
668
- text = f"Rumor: {text} maybe fake"
669
- microblog_posts.append(
670
- {
671
- "post_id": f"post_{i}",
672
- "user_id": poster,
673
- "canonical_user": alias_owner.get(poster, user.node_id),
674
- "text": text,
675
- "references": [],
676
- "reference_names": [],
677
- "mentions": [f"user_{self.rng.randint(0, self.config.n_users - 1)}"],
678
- "timestamp": 1000 + i,
679
- }
680
- )
681
-
682
- authored_posts: dict[str, str] = {}
683
- post_references: dict[str, list[str]] = {}
684
- for edge in graph.edges:
685
- if edge.rel == "authored_post":
686
- authored_posts[edge.dst] = edge.src
687
- elif edge.rel == "references" and edge.src.startswith("post_"):
688
- post_references.setdefault(edge.src, []).append(edge.dst)
689
-
690
- for post_id, author_id in authored_posts.items():
691
- refs = post_references.get(post_id, [])
692
- ref_names = [node_names.get(ref, ref) for ref in refs]
693
- author_label = node_names.get(author_id, author_id)
694
- text_parts = [f"{post_id} update from {author_label}"]
695
- if ref_names:
696
- text_parts.append("references " + ", ".join(ref_names))
697
- if refs:
698
- text_parts.append("ids " + ", ".join(refs))
699
- post_payload = {
700
- "post_id": post_id,
701
- "user_id": author_id,
702
- "canonical_user": alias_owner.get(author_id, author_id),
703
- "text": ". ".join(text_parts),
704
- "references": refs,
705
- "reference_names": ref_names,
706
- "mentions": [],
707
- "timestamp": 5000 + len(microblog_posts),
708
- }
709
- existing_idx = next((idx for idx, row in enumerate(microblog_posts) if row["post_id"] == post_id), None)
710
- if existing_idx is None:
711
- microblog_posts.append(post_payload)
712
- else:
713
- microblog_posts[existing_idx] = post_payload
714
-
715
- forum_threads: list[dict] = []
716
- for i in range(max(8, self.config.n_users // 3)):
717
- author = self.rng.choice(users).node_id
718
- forum_threads.append(
719
- {
720
- "thread_id": f"thr_{i}",
721
- "topic": self.rng.choice(["security", "startup", "ai", "infra"]),
722
- "author_id": author,
723
- "comments": [
724
- {"user_id": self.rng.choice(users).node_id, "text": "Following this."},
725
- {"user_id": self.rng.choice(users).node_id, "text": "Interesting link."},
726
- ],
727
- "references": [],
728
- "discusses": [],
729
- }
730
- )
731
-
732
- authored_threads: dict[str, str] = {}
733
- thread_refs: dict[str, list[str]] = {}
734
- thread_discusses: dict[str, list[str]] = {}
735
- for edge in graph.edges:
736
- if edge.rel == "authored_thread":
737
- authored_threads[edge.dst] = edge.src
738
- elif edge.rel == "references" and edge.src.startswith(("thr_", "thread_")):
739
- thread_refs.setdefault(edge.src, []).append(edge.dst)
740
- elif edge.rel == "discusses" and edge.src.startswith(("thr_", "thread_")):
741
- thread_discusses.setdefault(edge.src, []).append(edge.dst)
742
-
743
- for thread_id, author_id in authored_threads.items():
744
- node = graph.nodes.get(thread_id)
745
- refs = thread_refs.get(thread_id, [])
746
- discussed = thread_discusses.get(thread_id, [])
747
- comments = []
748
- for ref in refs:
749
- comments.append({"user_id": author_id, "text": f"Reference: {node_names.get(ref, ref)} ({ref})"})
750
- for item in discussed:
751
- comments.append({"user_id": author_id, "text": f"Discusses: {node_names.get(item, item)} ({item})"})
752
- thread_payload = {
753
- "thread_id": thread_id,
754
- "topic": str((node.attrs or {}).get("topic", "seeded")) if node else "seeded",
755
- "author_id": author_id,
756
- "title": node_names.get(thread_id, thread_id),
757
- "comments": comments,
758
- "references": refs,
759
- "discusses": discussed,
760
- }
761
- existing_idx = next((idx for idx, row in enumerate(forum_threads) if row["thread_id"] == thread_id), None)
762
- if existing_idx is None:
763
- forum_threads.append(thread_payload)
764
- else:
765
- forum_threads[existing_idx] = thread_payload
766
-
767
- profiles: list[dict] = []
768
- for user in users:
769
- conns = [e.dst for e in graph.edges if e.src == user.node_id and e.rel == "connected_to"][:5]
770
- org_id = next((e.dst for e in graph.edges if e.src == user.node_id and e.rel == "works_at"), "")
771
- location_id = next((e.dst for e in graph.edges if e.src == user.node_id and e.rel == "located_in"), "")
772
- profiles.append(
773
- {
774
- "user_id": user.node_id,
775
- "name": user.attrs["name"],
776
- "org": user.attrs["org"],
777
- "org_id": org_id,
778
- "location": user.attrs["location"],
779
- "location_id": location_id,
780
- "alias_ids": sorted(user_aliases.get(user.node_id, [])),
781
- "connections": conns,
782
- "work_history": [user.attrs["org"]],
783
- }
784
- )
785
-
786
- for i in range(int(len(users) * self.config.red_herring_rate)):
787
- profiles.append(
788
- {
789
- "user_id": f"noise_{i}",
790
- "name": f"P{self.rng.randint(100,999)}",
791
- "org": self.rng.choice(["Stealth Co", "Unknown Ventures"]),
792
- "org_id": "",
793
- "location": self.rng.choice(["Remote", "Unknown"]),
794
- "location_id": "",
795
- "alias_ids": [],
796
- "connections": [],
797
- "work_history": [],
798
- }
799
- )
800
- return PlatformViews(microblog_posts, forum_threads, profiles, alias_lookup=alias_owner)
801
-
802
- def generate_tasks(self, graph: CanonicalGraph, views: PlatformViews, count: int = 12) -> list[TaskInstance]:
803
- tasks = self._seeded_tasks(graph)
804
- target_count = max(1, count, len(tasks))
805
-
806
- llm_budget = min(
807
- max(0, self.config.seeding.llm_generated_task_budget),
808
- max(0, target_count - len(tasks)),
809
- )
810
- if self.config.seeding.llm_generate_remaining_tasks and llm_budget > 0:
811
- tasks.extend(self._llm_generated_tasks(graph, count=llm_budget, start_idx=len(tasks)))
812
-
813
- if len(tasks) < target_count and self._template_fallback_allowed():
814
- tasks.extend(self._template_tasks(graph, count=target_count - len(tasks), start_idx=len(tasks)))
815
-
816
- if not tasks:
817
- tasks.extend(self._template_tasks(graph, count=target_count, start_idx=0))
818
-
819
- return tasks[:target_count]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scratch/osint/src/osint_env/domain/__init__.py DELETED
@@ -1,2 +0,0 @@
1
- """Core domain models."""
2
-
 
 
 
scratch/osint/src/osint_env/domain/models.py DELETED
@@ -1,185 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass, field
4
- from enum import Enum
5
- from typing import Any
6
-
7
- from pydantic import BaseModel, ConfigDict, Field
8
-
9
-
10
- class NodeType(str, Enum):
11
- USER = "user"
12
- ALIAS = "alias"
13
- ORG = "org"
14
- LOCATION = "location"
15
- POST = "post"
16
- THREAD = "thread"
17
- EVENT = "event"
18
-
19
-
20
- class ActionType(str, Enum):
21
- CALL_TOOL = "CALL_TOOL"
22
- ADD_EDGE = "ADD_EDGE"
23
- ANSWER = "ANSWER"
24
-
25
-
26
- @dataclass(slots=True)
27
- class Node:
28
- node_id: str
29
- node_type: NodeType
30
- attrs: dict[str, Any] = field(default_factory=dict)
31
-
32
-
33
- @dataclass(slots=True)
34
- class Edge:
35
- src: str
36
- rel: str
37
- dst: str
38
- confidence: float = 1.0
39
-
40
-
41
- @dataclass(slots=True)
42
- class CanonicalGraph:
43
- nodes: dict[str, Node] = field(default_factory=dict)
44
- edges: list[Edge] = field(default_factory=list)
45
-
46
-
47
- @dataclass(slots=True)
48
- class ToolCall:
49
- tool_name: str
50
- args: dict[str, Any]
51
-
52
-
53
- class Action(BaseModel):
54
- """Structured action payload used by OpenEnv step()."""
55
-
56
- model_config = ConfigDict(extra="forbid")
57
-
58
- action_type: ActionType
59
- payload: dict[str, Any] = Field(default_factory=dict)
60
-
61
- def __init__(self, *args: Any, **kwargs: Any) -> None:
62
- # Backward-compatible positional form: Action(action_type, payload)
63
- if args:
64
- if len(args) != 2:
65
- raise TypeError("Action() accepts either keyword fields or 2 positional args")
66
- if "action_type" in kwargs or "payload" in kwargs:
67
- raise TypeError("Action() cannot mix positional and keyword fields")
68
- kwargs["action_type"] = args[0]
69
- kwargs["payload"] = args[1]
70
- super().__init__(**kwargs)
71
-
72
-
73
- class Observation(BaseModel):
74
- """Typed observation payload returned by reset()/step()/state()."""
75
-
76
- model_config = ConfigDict(extra="forbid")
77
-
78
- tool_outputs: list[dict[str, Any]] = Field(default_factory=list)
79
- graph_snapshot: dict[str, Any] = Field(default_factory=dict)
80
- action_history: list[dict[str, Any]] = Field(default_factory=list)
81
- task: dict[str, Any] = Field(default_factory=dict)
82
-
83
-
84
- class Reward(BaseModel):
85
- """Typed reward payload for structured reward accounting."""
86
-
87
- model_config = ConfigDict(extra="forbid")
88
-
89
- value: float = 0.0
90
- components: dict[str, float] = Field(default_factory=dict)
91
-
92
-
93
- @dataclass(slots=True)
94
- class TaskInstance:
95
- task_id: str
96
- task_type: str
97
- question: str
98
- answer: str
99
- supporting_edges: list[Edge]
100
- metadata: dict[str, Any] = field(default_factory=dict)
101
-
102
-
103
- @dataclass(slots=True)
104
- class SeedNodeSpec:
105
- node_id: str
106
- node_type: NodeType | str
107
- attrs: dict[str, Any] = field(default_factory=dict)
108
-
109
-
110
- @dataclass(slots=True)
111
- class SeedEdgeSpec:
112
- src: str
113
- rel: str
114
- dst: str
115
- confidence: float = 1.0
116
-
117
-
118
- @dataclass(slots=True)
119
- class SeedQuestionSpec:
120
- question: str
121
- answer: str | None = None
122
- task_type: str = "seeded"
123
- supporting_edges: list[SeedEdgeSpec] = field(default_factory=list)
124
- metadata: dict[str, Any] = field(default_factory=dict)
125
-
126
-
127
- @dataclass(slots=True)
128
- class SeedingConfig:
129
- seeded_nodes: list[SeedNodeSpec] = field(default_factory=list)
130
- seeded_edges: list[SeedEdgeSpec] = field(default_factory=list)
131
- seeded_questions: list[SeedQuestionSpec] = field(default_factory=list)
132
- llm_generate_remaining_graph: bool = True
133
- llm_generate_remaining_tasks: bool = True
134
- llm_generated_edge_budget: int = 6
135
- llm_generated_task_budget: int = 8
136
- llm_generation_parallel: bool = True
137
- llm_generation_workers: int = 3
138
- llm_generation_retries: int = 2
139
- allow_template_fallback_on_llm_failure: bool = False
140
-
141
-
142
- @dataclass(slots=True)
143
- class SwarmConfig:
144
- enabled: bool = False
145
- max_agents: int = 3
146
- max_breadth: int = 2
147
- max_width: int = 2
148
- max_depth: int = 2
149
- planner_rounds: int = 2
150
- tools_per_agent: int = 1
151
-
152
-
153
- @dataclass(slots=True)
154
- class SpawnRewardConfig:
155
- lambda_parallel: float = 0.15
156
- lambda_finish: float = 0.20
157
- anneal: float = 1.0
158
- max_parallel_hint: int = 3
159
-
160
-
161
- @dataclass(slots=True)
162
- class LLMConfig:
163
- provider: str = "mock"
164
- model: str = "qwen3:2b"
165
- temperature: float = 0.1
166
- max_tokens: int = 256
167
- timeout_seconds: int = 240
168
- ollama_base_url: str = "http://127.0.0.1:11434"
169
- openai_base_url: str = "https://api.openai.com/v1"
170
- openai_api_key_env: str = "OPENAI_API_KEY"
171
- openai_api_key: str = ""
172
-
173
-
174
- @dataclass(slots=True)
175
- class EnvironmentConfig:
176
- n_users: int = 40
177
- alias_density: float = 0.35
178
- noise_level: float = 0.15
179
- red_herring_rate: float = 0.1
180
- max_steps: int = 18
181
- seed: int = 7
182
- seeding: SeedingConfig = field(default_factory=SeedingConfig)
183
- swarm: SwarmConfig = field(default_factory=SwarmConfig)
184
- spawn_reward: SpawnRewardConfig = field(default_factory=SpawnRewardConfig)
185
- llm: LLMConfig = field(default_factory=LLMConfig)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scratch/osint/src/osint_env/env/__init__.py DELETED
@@ -1,2 +0,0 @@
1
- """Environment package."""
2
-
 
 
 
scratch/osint/src/osint_env/env/environment.py DELETED
@@ -1,260 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass, field
4
- from typing import TYPE_CHECKING, Any
5
-
6
- from osint_env.data.generator import DatasetGenerator
7
- from osint_env.domain.models import Action, ActionType, Edge, EnvironmentConfig, Observation, TaskInstance
8
- from osint_env.env.openenv_compat import Env
9
- from osint_env.env.reward import (
10
- build_reward_model,
11
- compute_answer_reward,
12
- compute_edge_reward,
13
- compute_graph_f1,
14
- )
15
- from osint_env.memory.store import MemoryGraph, SemanticMemory
16
- from osint_env.platforms.tools import ToolRegistry
17
-
18
- if TYPE_CHECKING:
19
- from osint_env.llm.interface import LLMClient
20
-
21
-
22
- @dataclass(slots=True)
23
- class EpisodeState:
24
- task: TaskInstance
25
- task_index: int = 0
26
- difficulty: str = "hard"
27
- step_count: int = 0
28
- done: bool = False
29
- total_reward: float = 0.0
30
- tool_calls: int = 0
31
- redundant_tool_calls: int = 0
32
- action_history: list[dict[str, Any]] = field(default_factory=list)
33
- tool_outputs: list[dict[str, Any]] = field(default_factory=list)
34
- answer: str | None = None
35
- call_fingerprints: set[str] = field(default_factory=set)
36
- reward_components: dict[str, float] = field(default_factory=dict)
37
-
38
-
39
- class OSINTEnvironment(Env):
40
- def __init__(self, config: EnvironmentConfig, llm: "LLMClient | None" = None):
41
- super().__init__(
42
- name="OSINTEnvironment",
43
- state_space="json-observation",
44
- action_space=["CALL_TOOL", "ADD_EDGE", "ANSWER"],
45
- episode_max_length=config.max_steps,
46
- )
47
- self.config = config
48
- self.generator = DatasetGenerator(config, llm=llm)
49
- self.graph = self.generator.build_canonical_graph()
50
- self.views = self.generator.build_platform_views(self.graph)
51
- self.tasks = self.generator.generate_tasks(self.graph, self.views, count=24)
52
- self.reward_model = build_reward_model(self.graph)
53
- self.tools = ToolRegistry(self.views)
54
- self.memory_graph = MemoryGraph()
55
- self.semantic_memory = SemanticMemory()
56
- self._task_idx = 0
57
- self.state: EpisodeState | None = None
58
-
59
- @staticmethod
60
- def _normalize_difficulty(value: str) -> str:
61
- token = str(value or "").strip().lower()
62
- if token in {"easy", "e"}:
63
- return "easy"
64
- if token in {"mid", "medium", "m"}:
65
- return "medium"
66
- if token in {"high", "hard", "h"}:
67
- return "hard"
68
- return "hard"
69
-
70
- def _resolve_task_difficulty(self, task: TaskInstance, task_index: int) -> str:
71
- metadata = dict(task.metadata or {})
72
- if "difficulty" in metadata:
73
- return self._normalize_difficulty(str(metadata.get("difficulty", "")))
74
- if task_index < 10:
75
- return "easy"
76
- if task_index < 20:
77
- return "medium"
78
- return "hard"
79
-
80
- def reset(self) -> Observation:
81
- task_index = self._task_idx % len(self.tasks)
82
- task = self.tasks[task_index]
83
- self._task_idx += 1
84
- self.state = EpisodeState(
85
- task=task,
86
- task_index=task_index,
87
- difficulty=self._resolve_task_difficulty(task, task_index),
88
- )
89
- self.memory_graph = MemoryGraph()
90
- self.semantic_memory = SemanticMemory()
91
- return self._observation()
92
-
93
- def step(self, action: Action) -> tuple[Observation, float, bool, dict[str, Any]]:
94
- if self.state is None:
95
- raise RuntimeError("Call reset() before step().")
96
- if self.state.done:
97
- return self._observation(), 0.0, True, self._info()
98
-
99
- self.state.step_count += 1
100
- reward = 0.0
101
-
102
- if action.action_type == ActionType.CALL_TOOL:
103
- reward += self._handle_tool(action.payload)
104
- elif action.action_type == ActionType.ADD_EDGE:
105
- reward += self._handle_add_edge(action.payload)
106
- elif action.action_type == ActionType.ANSWER:
107
- reward += self._handle_answer(action.payload)
108
- else:
109
- reward -= 0.5
110
-
111
- if self.state.step_count >= self.config.max_steps and not self.state.done:
112
- self.state.done = True
113
- reward -= 0.3
114
-
115
- self.state.total_reward += reward
116
- self.state.action_history.append({"type": action.action_type.value, "payload": action.payload, "reward": reward})
117
- return self._observation(), reward, self.state.done, self._info()
118
-
119
- def _handle_tool(self, payload: dict[str, Any]) -> float:
120
- if self.state is None:
121
- return 0.0
122
- tool_name = payload["tool_name"]
123
- args = payload.get("args", {})
124
- fp = f"{tool_name}:{sorted(args.items())}"
125
- self.state.tool_calls += 1
126
- if fp in self.state.call_fingerprints:
127
- self.state.redundant_tool_calls += 1
128
- penalty = -0.2
129
- else:
130
- penalty = 0.05
131
- self.state.call_fingerprints.add(fp)
132
-
133
- invalid_tool_penalty = 0.0
134
- try:
135
- if tool_name == "search_memory":
136
- query = str(args.get("query", "")).strip()
137
- top_k = int(args.get("k", 5)) if str(args.get("k", "")).strip() else 5
138
- results = self.semantic_memory.search(query=query, k=max(1, top_k)) if query else []
139
- output = {"results": results, "count": len(results)}
140
- else:
141
- output = self.tools.call(tool_name, args)
142
- except Exception as exc:
143
- output = {"error": str(exc)}
144
- invalid_tool_penalty = -0.25
145
- self.state.tool_outputs.append({"tool": tool_name, "args": args, "output": output})
146
- self.semantic_memory.add(f"{tool_name} {args} {output}", {"tool": tool_name})
147
- relevance_bonus = 0.08 * self._tool_relevance(self.state.task, output)
148
- total = penalty + relevance_bonus + invalid_tool_penalty
149
- self._accumulate_reward_components(
150
- {
151
- "tool_novelty": penalty,
152
- "tool_relevance": relevance_bonus,
153
- "invalid_tool_penalty": invalid_tool_penalty,
154
- }
155
- )
156
- return total
157
-
158
- def _handle_add_edge(self, payload: dict[str, Any]) -> float:
159
- if self.state is None:
160
- return 0.0
161
- edge = Edge(payload["src"], payload["rel"], payload["dst"], float(payload.get("confidence", 1.0)))
162
- existing_edges = list(self.memory_graph.edges)
163
- added = self.memory_graph.add_edge(edge)
164
- if not added:
165
- self._accumulate_reward_components({"duplicate_edge_penalty": -0.15})
166
- return -0.15
167
-
168
- breakdown = compute_edge_reward(
169
- edge=edge,
170
- task=self.state.task,
171
- existing_edges=existing_edges,
172
- step_count=self.state.step_count,
173
- model=self.reward_model,
174
- graph=self.graph,
175
- difficulty=self.state.difficulty,
176
- )
177
- self._accumulate_reward_components(breakdown.to_dict())
178
- return breakdown.total
179
-
180
- def _handle_answer(self, payload: dict[str, Any]) -> float:
181
- if self.state is None:
182
- return 0.0
183
- proposed = str(payload.get("answer", "")).strip()
184
- self.state.answer = proposed
185
- self.state.done = True
186
- breakdown = compute_answer_reward(
187
- proposed_answer=proposed,
188
- task=self.state.task,
189
- pred_edges=self.memory_graph.edges,
190
- tool_outputs=self.state.tool_outputs,
191
- step_count=self.state.step_count,
192
- model=self.reward_model,
193
- difficulty=self.state.difficulty,
194
- )
195
- self._accumulate_reward_components(breakdown.to_dict())
196
- return breakdown.total
197
-
198
- def _tool_relevance(self, task: TaskInstance, output: dict[str, Any]) -> float:
199
- haystack = str(output).lower()
200
- clues = {task.answer.lower()}
201
- for edge in task.supporting_edges:
202
- clues.add(edge.src.lower())
203
- clues.add(edge.dst.lower())
204
- clues.add(edge.rel.lower())
205
- if not clues:
206
- return 0.0
207
- matches = sum(1 for token in clues if token in haystack)
208
- return matches / len(clues)
209
-
210
- def _accumulate_reward_components(self, values: dict[str, float]) -> None:
211
- if self.state is None:
212
- return
213
- for key, value in values.items():
214
- self.state.reward_components[key] = self.state.reward_components.get(key, 0.0) + float(value)
215
-
216
- def _observation(self) -> Observation:
217
- if self.state is None:
218
- raise RuntimeError("State is not initialized.")
219
- metadata = dict(self.state.task.metadata or {})
220
- grader = metadata.get("grader") if isinstance(metadata.get("grader"), dict) else None
221
- task_payload = {
222
- "task_id": self.state.task.task_id,
223
- "task_type": self.state.task.task_type,
224
- "question": self.state.task.question,
225
- "difficulty": self.state.difficulty,
226
- "grader": (
227
- dict(grader)
228
- if grader is not None
229
- else {
230
- "type": "difficulty_exact_match",
231
- "answer_type": "node_id",
232
- "case_sensitive": True,
233
- "reward_profile": self.state.difficulty,
234
- }
235
- ),
236
- }
237
- if "scenario" in metadata:
238
- task_payload["scenario"] = str(metadata.get("scenario", ""))
239
- return Observation(
240
- tool_outputs=self.state.tool_outputs[-5:],
241
- graph_snapshot=self.memory_graph.to_snapshot(),
242
- action_history=self.state.action_history[-10:],
243
- task=task_payload,
244
- )
245
-
246
- def _info(self) -> dict[str, Any]:
247
- if self.state is None:
248
- return {}
249
- return {
250
- "step_count": self.state.step_count,
251
- "difficulty": self.state.difficulty,
252
- "task_index": self.state.task_index,
253
- "total_reward": self.state.total_reward,
254
- "tool_calls": self.state.tool_calls,
255
- "redundant_tool_calls": self.state.redundant_tool_calls,
256
- "task_answer": self.state.task.answer,
257
- "agent_answer": self.state.answer,
258
- "graph_f1": compute_graph_f1(self.memory_graph.edges, self.state.task.supporting_edges),
259
- "reward_components": dict(self.state.reward_components),
260
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scratch/osint/src/osint_env/env/openenv_compat.py DELETED
@@ -1,20 +0,0 @@
1
- from __future__ import annotations
2
-
3
- try:
4
- from openenv.env import Env
5
- except ImportError:
6
- class Env:
7
- """Minimal fallback used when openenv is not installed locally."""
8
-
9
- def __init__(
10
- self,
11
- name: str,
12
- state_space: str,
13
- action_space: list[str],
14
- episode_max_length: int,
15
- ) -> None:
16
- self.name = name
17
- self.state_space = state_space
18
- self.action_space = action_space
19
- self.episode_max_length = episode_max_length
20
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scratch/osint/src/osint_env/env/reward.py DELETED
@@ -1,483 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import json
4
- import math
5
- import re
6
- from collections import Counter
7
- from dataclasses import asdict, dataclass
8
-
9
- from osint_env.domain.models import CanonicalGraph, Edge, TaskInstance
10
-
11
-
12
- @dataclass(slots=True)
13
- class RewardModel:
14
- relation_idf: dict[str, float]
15
- max_relation_idf: float
16
- hub_penalty: dict[str, float]
17
- max_hub_penalty: float
18
- type_priors: dict[tuple[str, str, str], float]
19
-
20
-
21
- @dataclass(slots=True)
22
- class EdgeRewardBreakdown:
23
- total: float
24
- global_accuracy: float
25
- soft_shaping: float
26
- efficiency: float
27
- diversity: float
28
- relation_informativeness: float
29
- entity_informativeness: float
30
- connectivity_gain: float
31
-
32
- def to_dict(self) -> dict[str, float]:
33
- return asdict(self)
34
-
35
-
36
- @dataclass(slots=True)
37
- class AnswerRewardBreakdown:
38
- total: float
39
- format_reward: float
40
- correctness: float
41
- knowledge_carrier: float
42
- knowledge_indexing: float
43
- connectivity: float
44
- graph_f1: float
45
- efficiency: float
46
- compactness: float
47
- relation_informativeness: float
48
- entity_informativeness: float
49
- repetition_penalty: float
50
-
51
- def to_dict(self) -> dict[str, float]:
52
- return asdict(self)
53
-
54
-
55
- def _normalize_difficulty(value: str) -> str:
56
- token = str(value or "").strip().lower()
57
- if token in {"easy", "e"}:
58
- return "easy"
59
- if token in {"mid", "medium", "m"}:
60
- return "medium"
61
- if token in {"high", "hard", "h"}:
62
- return "hard"
63
- return "hard"
64
-
65
-
66
- def build_reward_model(graph: CanonicalGraph) -> RewardModel:
67
- relation_freq: Counter[str] = Counter(e.rel for e in graph.edges)
68
- total_edges = max(1, len(graph.edges))
69
- relation_idf = {
70
- rel: math.log((1.0 + total_edges) / (1.0 + freq)) + 1.0 for rel, freq in relation_freq.items()
71
- }
72
- max_relation_idf = max(relation_idf.values()) if relation_idf else 1.0
73
-
74
- degree: Counter[str] = Counter()
75
- for edge in graph.edges:
76
- degree[edge.src] += 1
77
- degree[edge.dst] += 1
78
- hub_penalty = {node_id: math.log(1.0 + deg) for node_id, deg in degree.items()}
79
- max_hub_penalty = max(hub_penalty.values()) if hub_penalty else 1.0
80
-
81
- type_counts: Counter[tuple[str, str, str]] = Counter()
82
- rel_counts: Counter[str] = Counter()
83
- for edge in graph.edges:
84
- src = graph.nodes.get(edge.src)
85
- dst = graph.nodes.get(edge.dst)
86
- if src is None or dst is None:
87
- continue
88
- key = (str(src.node_type.value), edge.rel, str(dst.node_type.value))
89
- type_counts[key] += 1
90
- rel_counts[edge.rel] += 1
91
- type_priors = {
92
- key: count / max(1, rel_counts[key[1]]) for key, count in type_counts.items()
93
- }
94
-
95
- return RewardModel(
96
- relation_idf=relation_idf,
97
- max_relation_idf=max_relation_idf,
98
- hub_penalty=hub_penalty,
99
- max_hub_penalty=max_hub_penalty,
100
- type_priors=type_priors,
101
- )
102
-
103
-
104
- def edge_in_truth(edge: Edge, task: TaskInstance) -> bool:
105
- return any(e.src == edge.src and e.rel == edge.rel and e.dst == edge.dst for e in task.supporting_edges)
106
-
107
-
108
- def _cosine(a: Counter[str], b: Counter[str]) -> float:
109
- common = set(a) & set(b)
110
- num = sum(a[t] * b[t] for t in common)
111
- den = math.sqrt(sum(v * v for v in a.values())) * math.sqrt(sum(v * v for v in b.values()))
112
- return (num / den) if den else 0.0
113
-
114
-
115
- def _edge_signature(edge: Edge) -> Counter[str]:
116
- # Approximate path/edge embedding using relation and endpoint prefixes.
117
- src_prefix = edge.src.split("_", 1)[0]
118
- dst_prefix = edge.dst.split("_", 1)[0]
119
- return Counter({f"rel:{edge.rel}": 2, f"src:{src_prefix}": 1, f"dst:{dst_prefix}": 1})
120
-
121
-
122
- def _soft_fact_score(edge: Edge, model: RewardModel, graph: CanonicalGraph) -> float:
123
- if any(e.src == edge.src and e.rel == edge.rel and e.dst == edge.dst for e in graph.edges):
124
- return 1.0
125
-
126
- src = graph.nodes.get(edge.src)
127
- dst = graph.nodes.get(edge.dst)
128
- if src is None or dst is None:
129
- return 0.0
130
-
131
- type_key = (str(src.node_type.value), edge.rel, str(dst.node_type.value))
132
- prior = model.type_priors.get(type_key, 0.0)
133
-
134
- # A tiny domain heuristic: alias links are common and worth soft credit even without exact support edge.
135
- alias_bias = 0.2 if (edge.rel == "alias_of" and edge.src.startswith("alias_") and edge.dst.startswith("user_")) else 0.0
136
- relation_exists = any(e.rel == edge.rel for e in graph.edges)
137
- relation_bonus = 0.1 if relation_exists else 0.0
138
- return max(0.0, min(1.0, 0.1 + (0.65 * prior) + alias_bias + relation_bonus))
139
-
140
-
141
- def _normalized_relation_info(rel: str, model: RewardModel) -> float:
142
- idf = model.relation_idf.get(rel, 1.0)
143
- return idf / max(1e-6, model.max_relation_idf)
144
-
145
-
146
- def _normalized_entity_info(src: str, dst: str, model: RewardModel) -> float:
147
- src_h = model.hub_penalty.get(src, 0.0)
148
- dst_h = model.hub_penalty.get(dst, 0.0)
149
- mean_hub = (src_h + dst_h) / 2.0
150
- # UniRel-style preference for low-degree intermediates: lower hub penalty -> higher informativeness.
151
- return 1.0 - (mean_hub / max(1e-6, model.max_hub_penalty))
152
-
153
-
154
- def _is_reachable_undirected(edges: list[Edge], src: str, dst: str) -> bool:
155
- if src == dst:
156
- return True
157
- adj: dict[str, set[str]] = {}
158
- for edge in edges:
159
- adj.setdefault(edge.src, set()).add(edge.dst)
160
- adj.setdefault(edge.dst, set()).add(edge.src)
161
- seen = {src}
162
- stack = [src]
163
- while stack:
164
- node = stack.pop()
165
- for nxt in adj.get(node, set()):
166
- if nxt == dst:
167
- return True
168
- if nxt not in seen:
169
- seen.add(nxt)
170
- stack.append(nxt)
171
- return False
172
-
173
-
174
- def _connectivity_gain(edge: Edge, existing_edges: list[Edge]) -> float:
175
- # Reward edges that bridge disconnected regions and penalize already-connected shortcuts.
176
- if edge.src == edge.dst:
177
- return -0.06
178
- already_connected = _is_reachable_undirected(existing_edges, edge.src, edge.dst)
179
- if already_connected:
180
- return -0.03
181
- return 0.10
182
-
183
-
184
- def _sigmoid_temperature(value: float, temperature: float = 2.0) -> float:
185
- scaled = float(value) / max(1e-6, float(temperature))
186
- if scaled >= 0:
187
- z = math.exp(-scaled)
188
- return 1.0 / (1.0 + z)
189
- z = math.exp(scaled)
190
- return z / (1.0 + z)
191
-
192
-
193
- def compute_edge_reward(
194
- edge: Edge,
195
- task: TaskInstance,
196
- existing_edges: list[Edge],
197
- step_count: int,
198
- model: RewardModel,
199
- graph: CanonicalGraph,
200
- difficulty: str = "hard",
201
- ) -> EdgeRewardBreakdown:
202
- in_truth = edge_in_truth(edge, task)
203
- difficulty_level = _normalize_difficulty(difficulty)
204
-
205
- # DeepPath-inspired global accuracy term.
206
- global_accuracy = 0.85 if in_truth else -0.55
207
-
208
- # D18 reward shaping: R = Rb + (1 - Rb) * f, where f is a soft fact plausibility score.
209
- base_reward = 1.0 if in_truth else 0.0
210
- shaped = base_reward + ((1.0 - base_reward) * _soft_fact_score(edge, model, graph))
211
- soft_shaping = 0.30 * (shaped - 0.5)
212
-
213
- # DeepPath-inspired efficiency term: earlier useful edges are better.
214
- efficiency = 0.10 * (1.0 / max(1, step_count))
215
-
216
- # DeepPath-inspired diversity term: discourage repeated edge patterns.
217
- if not existing_edges:
218
- diversity = 0.08
219
- else:
220
- new_sig = _edge_signature(edge)
221
- avg_similarity = sum(_cosine(new_sig, _edge_signature(e)) for e in existing_edges) / len(existing_edges)
222
- novelty = 1.0 - avg_similarity
223
- diversity = 0.14 * (novelty - 0.5)
224
-
225
- # UniRel-style informativeness terms.
226
- relation_informativeness = 0.12 * (_normalized_relation_info(edge.rel, model) - 0.5)
227
- entity_informativeness = 0.12 * (_normalized_entity_info(edge.src, edge.dst, model) - 0.5)
228
-
229
- # Additional structural utility shaping for KG construction.
230
- connectivity_gain = _connectivity_gain(edge, existing_edges)
231
-
232
- if difficulty_level == "easy":
233
- global_accuracy = 0.75 if in_truth else -0.45
234
- soft_shaping = 0.0
235
- diversity = 0.0
236
- relation_informativeness = 0.0
237
- entity_informativeness = 0.0
238
- connectivity_gain = 0.0
239
- efficiency = 0.15 * (1.0 / max(1, step_count))
240
- elif difficulty_level == "medium":
241
- diversity = 0.0
242
- relation_informativeness = 0.0
243
- entity_informativeness = 0.0
244
-
245
- raw_total = (
246
- global_accuracy
247
- + soft_shaping
248
- + efficiency
249
- + diversity
250
- + relation_informativeness
251
- + entity_informativeness
252
- + connectivity_gain
253
- )
254
- total = _sigmoid_temperature(raw_total, temperature=2.0)
255
- return EdgeRewardBreakdown(
256
- total=total,
257
- global_accuracy=global_accuracy,
258
- soft_shaping=soft_shaping,
259
- efficiency=efficiency,
260
- diversity=diversity,
261
- relation_informativeness=relation_informativeness,
262
- entity_informativeness=entity_informativeness,
263
- connectivity_gain=connectivity_gain,
264
- )
265
-
266
-
267
- def _connectivity_ratio(pred_edges: list[Edge], task: TaskInstance) -> float:
268
- nodes = {e.src for e in task.supporting_edges} | {e.dst for e in task.supporting_edges}
269
- if len(nodes) <= 1:
270
- return 1.0
271
-
272
- adj: dict[str, set[str]] = {}
273
- for edge in pred_edges:
274
- adj.setdefault(edge.src, set()).add(edge.dst)
275
- adj.setdefault(edge.dst, set()).add(edge.src)
276
-
277
- start = next(iter(nodes))
278
- seen = {start}
279
- stack = [start]
280
- while stack:
281
- cur = stack.pop()
282
- for nxt in adj.get(cur, set()):
283
- if nxt not in seen:
284
- seen.add(nxt)
285
- stack.append(nxt)
286
- return len(seen & nodes) / max(1, len(nodes))
287
-
288
-
289
- def _knowledge_indexing_recall(task: TaskInstance, tool_outputs: list[dict[str, object]]) -> float:
290
- gold_terms = {task.answer.lower()}
291
- for edge in task.supporting_edges:
292
- gold_terms.add(edge.src.lower())
293
- gold_terms.add(edge.dst.lower())
294
- gold_terms.add(edge.rel.lower())
295
-
296
- serialized = json.dumps(tool_outputs).lower()
297
- covered = sum(1 for term in gold_terms if term and term in serialized)
298
- return covered / max(1, len(gold_terms))
299
-
300
-
301
- def _knowledge_carrier_reward(pred_edges: list[Edge], task: TaskInstance) -> float:
302
- pred = {(e.src, e.rel, e.dst) for e in pred_edges}
303
- truth = {(e.src, e.rel, e.dst) for e in task.supporting_edges}
304
- deducible = bool(truth & pred)
305
- return 0.4 if deducible else -0.2
306
-
307
-
308
- def _extract_query_entities(question: str) -> set[str]:
309
- pattern = r"\b(?:alias|user|org|loc|post|thr|thread|event)_[a-zA-Z0-9_]+\b"
310
- return set(re.findall(pattern, question))
311
-
312
-
313
- def _max_connected_seed_count(pred_edges: list[Edge], seeds: set[str]) -> int:
314
- if not seeds:
315
- return 0
316
- adj: dict[str, set[str]] = {}
317
- for edge in pred_edges:
318
- adj.setdefault(edge.src, set()).add(edge.dst)
319
- adj.setdefault(edge.dst, set()).add(edge.src)
320
-
321
- best = 1
322
- for seed in seeds:
323
- seen = {seed}
324
- stack = [seed]
325
- while stack:
326
- cur = stack.pop()
327
- for nxt in adj.get(cur, set()):
328
- if nxt not in seen:
329
- seen.add(nxt)
330
- stack.append(nxt)
331
- connected_seed_count = len(seeds & seen)
332
- best = max(best, connected_seed_count)
333
- return best
334
-
335
-
336
- def _unirel_connectivity_score(pred_edges: list[Edge], seeds: set[str]) -> float:
337
- # UniRel-style discrete connectivity range projected to [-1, 1] for stable weighting.
338
- n = len(seeds)
339
- if n <= 1:
340
- return 0.0
341
-
342
- connected = _max_connected_seed_count(pred_edges, seeds)
343
- raw = -math.floor(n / 2) + max(0, connected - 1)
344
- lo = -math.floor(n / 2)
345
- hi = math.ceil(n / 2) - 1
346
- if hi <= lo:
347
- return 0.0
348
- return ((raw - lo) / (hi - lo)) * 2.0 - 1.0
349
-
350
-
351
- def _subgraph_relation_informativeness(pred_edges: list[Edge], model: RewardModel | None) -> float:
352
- if not pred_edges or model is None:
353
- return 0.0
354
- avg = sum(_normalized_relation_info(edge.rel, model) for edge in pred_edges) / len(pred_edges)
355
- return avg - 0.5
356
-
357
-
358
- def _subgraph_entity_informativeness(pred_edges: list[Edge], model: RewardModel | None) -> float:
359
- if not pred_edges or model is None:
360
- return 0.0
361
- avg = sum(_normalized_entity_info(edge.src, edge.dst, model) for edge in pred_edges) / len(pred_edges)
362
- return avg - 0.5
363
-
364
-
365
- def _relation_repetition_ratio(pred_edges: list[Edge]) -> float:
366
- if len(pred_edges) <= 1:
367
- return 0.0
368
- rels = [edge.rel for edge in pred_edges]
369
- unique = len(set(rels))
370
- return 1.0 - (unique / len(rels))
371
-
372
-
373
- def _deducible_answer(proposed_answer: str, task: TaskInstance, pred_edges: list[Edge]) -> bool:
374
- if proposed_answer != task.answer:
375
- return False
376
- truth = {(edge.src, edge.rel, edge.dst) for edge in task.supporting_edges}
377
- pred = {(edge.src, edge.rel, edge.dst) for edge in pred_edges}
378
- if truth & pred:
379
- return True
380
-
381
- seeds = _extract_query_entities(task.question)
382
- if not seeds:
383
- return False
384
- for seed in seeds:
385
- if _is_reachable_undirected(pred_edges, seed, proposed_answer):
386
- return True
387
- return False
388
-
389
-
390
- def compute_answer_reward(
391
- proposed_answer: str,
392
- task: TaskInstance,
393
- pred_edges: list[Edge],
394
- tool_outputs: list[dict[str, object]],
395
- step_count: int,
396
- model: RewardModel | None = None,
397
- difficulty: str = "hard",
398
- ) -> AnswerRewardBreakdown:
399
- difficulty_level = _normalize_difficulty(difficulty)
400
-
401
- format_reward = 0.15 if proposed_answer else -0.55
402
- correctness = 1.15 if proposed_answer == task.answer else -1.0
403
-
404
- # AutoGraph-R1 style task utility decomposition.
405
- knowledge_carrier = 0.50 if _deducible_answer(proposed_answer, task, pred_edges) else -0.25
406
- knowledge_indexing = 0.45 * _knowledge_indexing_recall(task, tool_outputs)
407
-
408
- # UniRel-style connectivity over seed entities.
409
- seed_entities = _extract_query_entities(task.question)
410
- seed_entities.add(task.answer)
411
- connectivity = 0.30 * _unirel_connectivity_score(pred_edges, seed_entities)
412
-
413
- graph_f1 = 0.55 * compute_graph_f1(pred_edges, task.supporting_edges)
414
- efficiency = 0.12 * (1.0 / max(1, step_count))
415
-
416
- extra_edges = max(0, len(pred_edges) - len(task.supporting_edges))
417
- compactness = -0.05 * extra_edges
418
-
419
- relation_informativeness = 0.12 * _subgraph_relation_informativeness(pred_edges, model)
420
- entity_informativeness = 0.12 * _subgraph_entity_informativeness(pred_edges, model)
421
-
422
- # AutoGraph-R1 repetition control variant used in larger models.
423
- repetition_penalty = -0.10 * _relation_repetition_ratio(pred_edges)
424
-
425
- if difficulty_level == "easy":
426
- knowledge_carrier = 0.0
427
- knowledge_indexing = 0.25 * _knowledge_indexing_recall(task, tool_outputs)
428
- connectivity = 0.0
429
- graph_f1 = 0.0
430
- efficiency = 0.18 * (1.0 / max(1, step_count))
431
- compactness = 0.0
432
- relation_informativeness = 0.0
433
- entity_informativeness = 0.0
434
- repetition_penalty = 0.0
435
- elif difficulty_level == "medium":
436
- connectivity = 0.18 * _unirel_connectivity_score(pred_edges, seed_entities)
437
- graph_f1 = 0.35 * compute_graph_f1(pred_edges, task.supporting_edges)
438
- compactness = -0.04 * extra_edges
439
- relation_informativeness = 0.0
440
- entity_informativeness = 0.0
441
- repetition_penalty = 0.0
442
-
443
- raw_total = (
444
- format_reward
445
- + correctness
446
- + knowledge_carrier
447
- + knowledge_indexing
448
- + connectivity
449
- + graph_f1
450
- + efficiency
451
- + compactness
452
- + relation_informativeness
453
- + entity_informativeness
454
- + repetition_penalty
455
- )
456
- total = _sigmoid_temperature(raw_total, temperature=2.0)
457
- return AnswerRewardBreakdown(
458
- total=total,
459
- format_reward=format_reward,
460
- correctness=correctness,
461
- knowledge_carrier=knowledge_carrier,
462
- knowledge_indexing=knowledge_indexing,
463
- connectivity=connectivity,
464
- graph_f1=graph_f1,
465
- efficiency=efficiency,
466
- compactness=compactness,
467
- relation_informativeness=relation_informativeness,
468
- entity_informativeness=entity_informativeness,
469
- repetition_penalty=repetition_penalty,
470
- )
471
-
472
-
473
- def compute_graph_f1(pred_edges: list[Edge], truth_edges: list[Edge]) -> float:
474
- pred = {(e.src, e.rel, e.dst) for e in pred_edges}
475
- truth = {(e.src, e.rel, e.dst) for e in truth_edges}
476
- if not pred and not truth:
477
- return 1.0
478
- if not pred or not truth:
479
- return 0.0
480
- tp = len(pred & truth)
481
- p = tp / len(pred) if pred else 0.0
482
- r = tp / len(truth) if truth else 0.0
483
- return (2 * p * r / (p + r)) if (p + r) else 0.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scratch/osint/src/osint_env/env/spawn_reward_hooks.py DELETED
@@ -1,93 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import math
4
-
5
-
6
- def critical_steps(main_steps: list[int], parallel_subagent_steps: list[list[int]]) -> int:
7
- """Compute critical-step latency proxy used in Kimi-style PARL shaping.
8
-
9
- For each stage t, we add:
10
- Smain(t) + max_i Ssub,i(t)
11
- where Ssub,i(t) is the i-th sub-agent step count for that stage.
12
- """
13
- if len(main_steps) != len(parallel_subagent_steps):
14
- raise ValueError("main_steps and parallel_subagent_steps must have the same length")
15
-
16
- total = 0
17
- for stage_main, stage_sub in zip(main_steps, parallel_subagent_steps):
18
- main = max(0, int(stage_main))
19
- longest_sub = max((max(0, int(v)) for v in stage_sub), default=0)
20
- total += main + longest_sub
21
- return total
22
-
23
-
24
- def parl_style_spawn_reward(
25
- task_outcome_reward: float,
26
- spawn_count: int,
27
- finished_subtasks: int,
28
- critical_steps: int,
29
- lambda_parallel: float = 0.15,
30
- lambda_finish: float = 0.20,
31
- anneal: float = 1.0,
32
- breadth: int | None = None,
33
- depth: int | None = None,
34
- max_parallel_hint: int | None = None,
35
- ) -> float:
36
- """Kimi K2.5 inspired PARL reward utility for future multi-agent branches.
37
-
38
- This helper intentionally does not orchestrate agents. It only exposes the reward shape:
39
-
40
- r_parl = r_perf + a * (lambda_parallel * r_parallel + lambda_finish * r_finish + r_latency)
41
-
42
- where:
43
- - r_parallel encourages non-zero agent spawning (avoids serial collapse)
44
- - r_finish rewards meaningful completion, preventing spawn-only reward hacking
45
- - r_latency favors lower critical-step execution paths
46
-
47
- The optional breadth/depth controls are small shaping terms for future branches where
48
- orchestration state includes tree shape telemetry.
49
- """
50
- spawn_count = max(0, int(spawn_count))
51
- finished_subtasks = max(0, int(finished_subtasks))
52
- critical_steps = max(1, int(critical_steps))
53
- anneal = max(0.0, min(1.0, anneal))
54
- lambda_parallel = max(0.0, float(lambda_parallel))
55
- lambda_finish = max(0.0, float(lambda_finish))
56
- breadth = max(0, int(breadth or 0))
57
- depth = max(0, int(depth or 0))
58
- max_parallel_hint = max(0, int(max_parallel_hint or 0))
59
-
60
- if spawn_count == 0:
61
- r_parallel = 0.0
62
- r_finish = 0.0
63
- else:
64
- # Saturating incentive for parallelism so reward cannot grow unbounded with spawns.
65
- r_parallel = math.tanh(spawn_count / 4.0)
66
- if max_parallel_hint > 0:
67
- utilization = min(1.0, spawn_count / max_parallel_hint)
68
- r_parallel *= (0.7 + (0.3 * utilization))
69
-
70
- r_finish = min(1.0, finished_subtasks / spawn_count)
71
-
72
- if breadth > 0:
73
- breadth_bonus = 0.04 * math.tanh(breadth / 6.0)
74
- else:
75
- breadth_bonus = 0.0
76
-
77
- if depth > 0:
78
- # Mild depth penalty discourages brittle over-decomposition chains.
79
- depth_penalty = -0.03 * math.tanh(max(0, depth - 1) / 4.0)
80
- else:
81
- depth_penalty = 0.0
82
-
83
- # Optional latency shaping hook using critical steps (higher is worse).
84
- r_latency = 0.05 * (1.0 / critical_steps)
85
-
86
- auxiliary = (
87
- (lambda_parallel * r_parallel)
88
- + (lambda_finish * r_finish)
89
- + r_latency
90
- + breadth_bonus
91
- + depth_penalty
92
- )
93
- return float(task_outcome_reward) + (anneal * auxiliary)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scratch/osint/src/osint_env/eval/__init__.py DELETED
@@ -1,2 +0,0 @@
1
- """Evaluation package."""
2
-
 
 
 
scratch/osint/src/osint_env/eval/leaderboard.py DELETED
@@ -1,83 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import json
4
- from datetime import datetime, timezone
5
- from pathlib import Path
6
- from typing import Any
7
-
8
-
9
- def _utc_now() -> str:
10
- return datetime.now(tz=timezone.utc).replace(microsecond=0).isoformat()
11
-
12
-
13
- def load_leaderboard(path: str | Path) -> list[dict[str, Any]]:
14
- file_path = Path(path)
15
- if not file_path.exists():
16
- return []
17
- with file_path.open("r", encoding="utf-8") as f:
18
- data = json.load(f)
19
- if not isinstance(data, list):
20
- return []
21
- return data
22
-
23
-
24
- def save_leaderboard(path: str | Path, records: list[dict[str, Any]]) -> None:
25
- file_path = Path(path)
26
- file_path.parent.mkdir(parents=True, exist_ok=True)
27
- with file_path.open("w", encoding="utf-8") as f:
28
- json.dump(records, f, indent=2, sort_keys=True)
29
-
30
-
31
- def _metric_value(record: dict[str, Any], sort_by: str) -> float:
32
- metrics = record.get("metrics", {})
33
- return float(metrics.get(sort_by, 0.0))
34
-
35
-
36
- def sorted_leaderboard(records: list[dict[str, Any]], sort_by: str = "leaderboard_score") -> list[dict[str, Any]]:
37
- return sorted(records, key=lambda r: _metric_value(r, sort_by), reverse=True)
38
-
39
-
40
- def append_leaderboard_record(
41
- path: str | Path,
42
- summary: dict[str, Any],
43
- episodes: int,
44
- run_name: str | None = None,
45
- config: dict[str, Any] | None = None,
46
- ) -> dict[str, Any]:
47
- records = load_leaderboard(path)
48
- run_id = f"run_{len(records) + 1:04d}"
49
- record = {
50
- "run_id": run_id,
51
- "run_name": run_name or run_id,
52
- "created_at": _utc_now(),
53
- "episodes": int(episodes),
54
- "config": config or {},
55
- "metrics": summary,
56
- }
57
- records.append(record)
58
- save_leaderboard(path, records)
59
- return record
60
-
61
-
62
- def render_leaderboard_table(records: list[dict[str, Any]], top_k: int = 20, sort_by: str = "leaderboard_score") -> str:
63
- ranked = sorted_leaderboard(records, sort_by=sort_by)[:top_k]
64
- header = "| rank | run | score | success | graph_f1 | retrieval | structural | spawn | reward | tool_eff |\n"
65
- sep = "|---|---|---:|---:|---:|---:|---:|---:|---:|---:|\n"
66
- rows: list[str] = []
67
- for idx, rec in enumerate(ranked, start=1):
68
- m = rec.get("metrics", {})
69
- rows.append(
70
- "| {rank} | {run} | {score:.4f} | {succ:.3f} | {f1:.3f} | {retrieval:.3f} | {structural:.3f} | {spawn:.3f} | {reward:.3f} | {tool:.3f} |".format(
71
- rank=idx,
72
- run=rec.get("run_name", rec.get("run_id", "run")),
73
- score=float(m.get("leaderboard_score", 0.0)),
74
- succ=float(m.get("task_success_rate", 0.0)),
75
- f1=float(m.get("avg_graph_f1", 0.0)),
76
- retrieval=float(m.get("retrieval_signal", 0.0)),
77
- structural=float(m.get("structural_signal", 0.0)),
78
- spawn=float(m.get("spawn_signal", 0.0)),
79
- reward=float(m.get("avg_reward", 0.0)),
80
- tool=float(m.get("tool_efficiency", 0.0)),
81
- )
82
- )
83
- return header + sep + "\n".join(rows)