Skip to content

Commit e334475

Browse files
author
Alexander Fischer
committed
refactored server.py for better coherence; improved system prompts
1 parent 133a7a1 commit e334475

9 files changed

Lines changed: 52 additions & 39 deletions

File tree

litellm_config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ model_list:
33
litellm_params:
44
model: openai/gpt-5.6-luna
55
api_key: "os.environ/CLIMATECLAW_OPENAI_API_KEY"
6-
reasoning_effort: "low"
6+
reasoning_effort: "medium"
77
- model_name: gpt-4.1
88
litellm_params:
99
model: openai/gpt-4.1

scripts/dev_evaluate_tool.py

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import time
99
from collections import defaultdict
1010
from dataclasses import dataclass
11+
from datetime import datetime
1112
from pathlib import Path
1213

1314
from matplotlib import pyplot as plt
@@ -63,16 +64,17 @@
6364
SILENCE_LOGGING = True
6465

6566
# run evaluation of tool calls (TP, FP, FN, accuracy, precision, recall)
66-
EVAL_TOOL_CALLS = True
67+
EVAL_TOOL = True
6768
PLOT_METRICS = True # plot metrics as bar charts at the end of the benchmark
6869

69-
## evaluation suite, consisting of direct questions about a plugin + indirect ones that are more widely phrased
70+
## evaluation suite: direct questions about a plugin + indirect ones (more widely phrased)
7071
path_to_prompts = Path(__file__).parent / "evaluation" / "benchmark_prompts.json"
7172
with open(path_to_prompts, "r", encoding="utf-8") as f:
7273
BENCHMARK = {k.lower(): v for k, v in json.load(f).items()}
7374
# BENCHMARK = {
7475
# "leadtimeselektor": [
7576
# "How does the 'leadtimeselektor' plugin work from a high-level perspective?",
77+
# "How can I best extract lead times from decadal climate predictions?",
7678
# "How are you?",
7779
# ]
7880
# }
@@ -264,7 +266,7 @@ async def run_benchmark_for_prompt(
264266
tool_args = [r.tool_args for r in results]
265267
tool_outputs = [r.tool_output for r in results]
266268

267-
if EVAL_TOOL_CALLS:
269+
if EVAL_TOOL:
268270
metrics_dict = _evaluate_tool_call_results(
269271
tool_names, tool_args, tool_outputs, plugin
270272
)
@@ -275,8 +277,10 @@ async def run_benchmark_for_prompt(
275277

276278

277279
def plot_metrics(avg_metrics: dict[str, float], save_dir: Path) -> None:
280+
current_date = datetime.today().strftime("%Y-%m-%d")
278281
# pie chart for tool/plugin TP, FP, FN
279282
fig, axs = plt.subplots(1, 2, figsize=(12, 6))
283+
metrics = ["TP", "FP", "FN"]
280284
tool_metrics = {
281285
"True Positives": avg_metrics.get("true_pos_tool", 0),
282286
"False Positives": avg_metrics.get("false_pos_tool", 0),
@@ -287,16 +291,22 @@ def plot_metrics(avg_metrics: dict[str, float], save_dir: Path) -> None:
287291
"False Positives": avg_metrics.get("false_pos_plugin", 0),
288292
"False Negatives": avg_metrics.get("false_neg_plugin", 0),
289293
}
290-
axs[0].pie(tool_metrics.values(), labels=tool_metrics.keys(), autopct="%1.1f%%")
291-
axs[0].set_title("Tool Call Metrics")
292-
axs[1].pie(plugin_metrics.values(), labels=plugin_metrics.keys(), autopct="%1.1f%%")
293-
axs[1].set_title("Plugin Call Metrics")
294+
charts = [tool_metrics, plugin_metrics]
295+
titles = ["Tool Call", "Plugin Call"]
296+
for ax, chart, title in zip(axs, charts, titles):
297+
pie = ax.pie(
298+
chart.values(),
299+
autopct="%1.1f%%",
300+
textprops=dict(fontsize=14, color="white"),
301+
)
302+
ax.legend(pie.wedges, metrics, title="Metrics", loc="best", fontsize=14)
303+
ax.set_title(title, fontsize=16)
294304
plt.suptitle(
295305
f"Evaluation Metrics for 'plugin_code_search' tool (Model: {MODEL}, Runs/Query: 30)"
296306
)
297307
plt.tight_layout()
298-
plt.subplots_adjust(wspace=0.4)
299-
plt.savefig(save_dir / "tool_evaluation_pie_charts.png")
308+
plt.subplots_adjust(wspace=0.2)
309+
plt.savefig(save_dir / f"tool_evaluation_pie_charts_{current_date}.png", dpi=400)
300310

301311
# bar chart for tool/plugin accuracy, precision, recall
302312
fig, axs = plt.subplots(1, 2, figsize=(12, 6))
@@ -322,7 +332,7 @@ def plot_metrics(avg_metrics: dict[str, float], save_dir: Path) -> None:
322332
)
323333
plt.tight_layout()
324334
plt.subplots_adjust(wspace=0.3)
325-
plt.savefig(save_dir / "tool_evaluation_metrics.png")
335+
plt.savefig(save_dir / f"tool_evaluation_metrics_{current_date}.png", dpi=400)
326336

327337

328338
async def main() -> None:
@@ -340,16 +350,16 @@ async def main() -> None:
340350
progress.update(RUNS)
341351

342352
# compute average metrics across all prompts
343-
if EVAL_TOOL_CALLS:
353+
if EVAL_TOOL:
344354
avg_metrics = {k: sum(v) / len(v) for k, v in eval_dict.items()}
345355
print(
346356
f"\n=== Average metrics [in %] across all prompts ({RUNS} runs/prompt) ==="
347357
)
348358
for k, v in avg_metrics.items():
349-
print(f"{k}: {v:.1%}")
359+
print(f" {k:<17}: {v:.1%}")
350360

351361
# plot metrics as bar charts (one for tool, one for plugin)
352-
if PLOT_METRICS and EVAL_TOOL_CALLS:
362+
if PLOT_METRICS and EVAL_TOOL:
353363
eval_dir = Path(__file__).parent / "evaluation"
354364
eval_dir.mkdir(parents=True, exist_ok=True)
355365
plot_metrics(avg_metrics, eval_dir)
-29.5 KB
Binary file not shown.
166 KB
Loading
-47 KB
Binary file not shown.
214 KB
Loading

src/climateclaw/prompt_library/gpt_5/starting_prompt.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# ClimateClaw Starting Prompt
1+
# ClimateClaw System Prompt
22

33
## A. IDENTITY & SCOPE
44

@@ -178,15 +178,16 @@ Users may provide paths such as: `/work/bm1159/XCES/xces-work/k204225/MYWORK`. T
178178

179179
## I. FORMATTING
180180

181-
- For equations, use Markdown math:
182-
- in-line: $E = mc^2$ or \\(E = mc^2\\) (double backslash to properly escape)
181+
- For equations, use Markdown math delimiters with an additional backslash to escape:
182+
- in-line: \$E = mc^2\$ or \\(E = mc^2\\)
183183
- as math block: $$\nabla \cdot \vec{u} = 0$$
184184
- For code, use Markdown code formatting:
185185
- in-line: `print("hello world")`
186186
- as code block:
187187

188188
```python
189189
import xarray as xr
190+
190191
ds = xr.open_dataset("file.nc")
191192
```
192193

src/climateclaw/prompt_library/gpt_5/summary_prompt.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
- Do not invent facts, paths, dataset availability, tool outputs, URLs, or analysis results. Base conclusions on user-provided information, tool output, or loaded data and metadata. State missing evidence plainly and make the smallest useful discovery call or ask for focused clarification when required.
66
- For requests requiring data access, numerical analysis, visualization, or file generation, give a short numbered plan and immediately call the appropriate tool. For conceptual questions that need no tools, answer directly. After a tool call, let its output determine the next action and do not claim success without confirmation.
77
- Use `code_interpreter` for all Python execution: Freva databrowser queries, data loading, calculations, plotting, and saving files. Import required libraries explicitly. For Freva, include `import freva_client`, use `host='nextgems.dkrz.de'`, inspect metadata before computation, and discover facets before making uncertain dataset or CMOR-variable selections. Use ERA5 reanalysis by default when the dataset is unspecified.
8-
- Use the relevant stages only: *discover/load → inspect metadata → compute → validate → plot/save*. Report selections, aggregation, and units when they materially affect the interpretation. Prefer scoped operations and real user-provided or Freva-discovered data over synthetic data.
8+
- Use the relevant stages only: *discover/load → inspect metadata → compute → validate → plot*. Report selections, aggregation, and units when they materially affect the interpretation. Prefer scoped operations and real user-provided or Freva-discovered data over synthetic data.
99
- For DKRZ/HPC, Slurm, or ICON documentation, use `web_search` and prefer official sources. Include inline citations with the URLs used.
1010
- Use `plugin_code_search` when the user asks about a Freva plugin's logic, usage, configuration, or adaptation, and proactively for complex regional, decadal, or extreme-event analyses where plugin-encoded methodology may apply. Skip it for simple generic analysis. Ground explanations only in returned code context; when relevant, identify modules, classes, and functions and link repository paths on the `levante` branch.
1111
- When a tool action fails, make one focused correction and retry when clear. For an interpreter timeout, reduce scope before retrying; use HPC/Slurm documentation only when an infrastructure, scheduler, or resource problem is indicated.

src/climateclaw/tools/plugin_code_search/server.py

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -227,8 +227,8 @@ async def select_relevant_files(
227227
f"Task: You are selecting the most relevant files from the '{plugin}' Freva plugin repository.\n\n"
228228
"Selection rules:\n"
229229
"- Prioritize files that seem most relevant to answer the query intent.\n"
230-
"- For high level usage/configuration questions, prioritize wrapper/config files and README/docs.\n"
231-
"- For questions about implementation logic, prioritize core source code modules.\n"
230+
"- For high level usage/configuration questions, prioritize wrapper/config files, README and docs.\n"
231+
"- For questions about implementation logic, prioritize source code modules.\n"
232232
"- Exclude tests, examples, generated files, and any '__init__.py'.\n"
233233
f"- Return ONLY a valid JSON array of file path strings from the provided list, with at most {MAX_RELEVANT_FILES} items. Output nothing but the JSON array."
234234
f"Repository file list:\n{file_tree}\n\n"
@@ -268,13 +268,15 @@ async def select_relevant_files(
268268
return [p for p in selected_files if p in set(file_paths)][:MAX_RELEVANT_FILES]
269269

270270

271-
async def collect_plugin_context(plugin: str, project_id: int, user_query: str) -> str:
271+
async def collect_plugin_context(
272+
plugin: str, project: str, project_id: int, user_query: str
273+
) -> str:
272274
"""
273275
Three-stage context retrieval of code base:
274276
1. Ask LLM which files are most relevant for the user's query.
275277
2. Fetch those files, then scan for imports to identify dependent modules.
276278
3. Fetch the dependencies and return the combined contents.
277-
Returns a string containing the concatenated relevant code snippets,
279+
Returns a string containing the concatenated relevant source code files,
278280
separated by file and with a header.
279281
"""
280282

@@ -288,12 +290,17 @@ def _log_stage(stage: str, files: list[str]):
288290
files,
289291
)
290292

293+
header = (
294+
f"Relevant retrieved code of the '{plugin}' plugin "
295+
f"(https://gitlab.dkrz.de/{project}/plugins4freva/{plugin}):\n\n"
296+
)
297+
291298
# ── Stage 0: fetch the repository tree with all files ────────────────────
292299
file_paths = fetch_repo_tree(project_id)
293300
if not file_paths:
294301
return "(repository is empty)"
295302

296-
# ── Stage 1: ask the LLM to select relevant files ─────────────────────────
303+
# ── Stage 1: let LLM select relevant files ─────────────────────────
297304
base_files = await select_relevant_files(plugin, user_query, file_paths)
298305
_log_stage("Initial", base_files)
299306

@@ -305,10 +312,11 @@ def _log_stage(stage: str, files: list[str]):
305312
dep_files = await select_relevant_files(plugin, init_code, tree_remaining, dep=True)
306313
_log_stage("Dependency", dep_files)
307314
if not dep_files:
308-
return init_code
315+
return header + init_code
309316

310317
dep_code = fetch_plugin_code(project_id, dep_files, MAX_TOTAL_CODE_CHARS // 3)
311-
return init_code + "\n\n# ── Dependency files ──\n\n" + dep_code
318+
code_content = init_code + "\n\n### ── Dependency files ── ###\n\n" + dep_code
319+
return header + code_content
312320

313321

314322
def validate_plugin_call(
@@ -407,25 +415,19 @@ async def plugin_code_search(user_query: str) -> str:
407415
str: Relevant code context fetched from source files of the plugin repository;
408416
or an error message if the plugin call is not authorized / code retrieval fails.
409417
"""
410-
plugin_name, project_name = await detect_plugin_project(user_query)
411-
# return f"plugin_name: {plugin_name}, project_name: {project_name}" # only for benchmarking the plugin detection stage
418+
plugin, project = await detect_plugin_project(user_query)
419+
# return f"plugin_name: {plugin}, project_name: {project}" # only for benchmarking the plugin detection stage
412420

413421
# Validate the plugin call
414-
project_id = get_project_id(plugin_name, project_name)
415-
result, message = validate_plugin_call(plugin_name, project_name, project_id)
422+
project_id = get_project_id(plugin, project)
423+
result, message = validate_plugin_call(plugin, project, project_id)
416424
if not result:
417425
return message
418426

419427
# Fetch the plugin code and return it with a header
420-
logger.info("Fetching source code for plugin '%s'", plugin_name)
428+
logger.info("Fetching source code for plugin '%s'", plugin)
421429
try:
422-
code_content = await collect_plugin_context(plugin_name, project_id, user_query) # type: ignore
430+
return await collect_plugin_context(plugin, project, project_id, user_query) # type: ignore
423431
except Exception as e:
424-
logger.warning("Failed to fetch plugin code for '%s': %s", plugin_name, e)
425-
return f"Failed to retrieve source code for plugin '{plugin_name}': {e}"
426-
427-
header = (
428-
f"Relevant retrieved code of the '{plugin_name}' plugin "
429-
f"(https://gitlab.dkrz.de/{project_name}/plugins4freva/{plugin_name}):\n\n"
430-
)
431-
return header + code_content
432+
logger.warning("Failed to fetch plugin code for '%s': %s", plugin, e)
433+
return f"Failed to retrieve source code for plugin '{plugin}': {e}"

0 commit comments

Comments
 (0)