diff --git a/.gitignore b/.gitignore index 8899d30..3d6afb7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,9 @@ venv/ __pycache__/ *.pyc .DS_Store +.pytest_cache/ runs/* !runs/.gitkeep .streamlit/secrets.toml +figures/ prompts/library/.cache/ diff --git a/app/ui_agent.py b/app/ui_agent.py index 1e7916b..6590d81 100644 --- a/app/ui_agent.py +++ b/app/ui_agent.py @@ -149,6 +149,7 @@ def __getattr__(self, name: str): ensure_skip_acquire_forcings_when_local_forcing, ensure_skip_model_agnostic_when_local_preprocessing, ensure_skip_process_observed_when_local_streamflow, + ensure_create_pour_point_before_define_domain, extract_explicit_domain_name_from_request, extract_station_id_from_request, resolve_station_id_from_plan, @@ -385,6 +386,54 @@ def format_datetime_value(date_value, time_value) -> str: return f"{date_value:%Y-%m-%d} {time_value:%H:%M}" +def _is_ui_placeholder_experiment_date(value: str, *, which: str) -> bool: + from server.core.plan_rules import UI_DEFAULT_EXPERIMENT_END, UI_DEFAULT_EXPERIMENT_START + + value = s(value) + if which == "start": + return value in {UI_DEFAULT_EXPERIMENT_START, UI_DEFAULT_EXPERIMENT_START[:10]} + return value in {UI_DEFAULT_EXPERIMENT_END, UI_DEFAULT_EXPERIMENT_END[:10]} + + +def commit_experiment_datetime_from_widgets( + *, + prev_value: str, + widget_value: str, + which: str, +) -> str: + """Keep Input-tab placeholder dates out of session/plan until the user or plan sets them.""" + prev_value = s(prev_value) + widget_value = s(widget_value) + if prev_value: + return widget_value + if widget_value and not _is_ui_placeholder_experiment_date(widget_value, which=which): + return widget_value + return "" + + +def ui_experiment_dates_for_plan() -> tuple[str, str]: + """Return dates to write into the plan, omitting uncommitted UI placeholders.""" + from server.core.plan_rules import request_mentions_experiment_dates + + tstart = s(st.session_state.tstart) + tend = s(st.session_state.tend) + prompt = user_prompt_for_metadata() or s(st.session_state.get("nl_request", "")) + prompt_has_dates = request_mentions_experiment_dates(prompt) + if tstart and ( + prompt_has_dates or not _is_ui_placeholder_experiment_date(tstart, which="start") + ): + pass + else: + tstart = "" + if tend and ( + prompt_has_dates or not _is_ui_placeholder_experiment_date(tend, which="end") + ): + pass + else: + tend = "" + return tstart, tend + + def bump_input_panel_widget_versions() -> None: """Refresh Input-tab widgets that cache values by Streamlit widget key.""" bump_all_input_widget_versions() @@ -1484,24 +1533,33 @@ def build_symfluence_step_cmd(step: str, config_path: Path) -> list[str]: ALL_GEMINI_MODELS = [m for group in GEMINI_MODELS.values() for m in group] CLAUDE_MODELS = { - "Claude 4.x (Recommended)": [ - "claude-sonnet-4-20250514", - "claude-opus-4-20250514", + "Claude 5 (Recommended)": ["claude-sonnet-5"], + "Claude 4.x": [ + "claude-opus-4-8", + "claude-sonnet-4-6", + "claude-sonnet-4-5", ], - "Claude 3.7": ["claude-3-7-sonnet-latest"], - "Claude 3.5 (Legacy)": [ + "Claude 3.x (Legacy)": [ + "claude-3-7-sonnet-latest", "claude-3-5-sonnet-latest", - "claude-3-5-haiku-latest", + "claude-haiku-4-5", ], } ALL_CLAUDE_MODELS = [m for group in CLAUDE_MODELS.values() for m in group] CLAUDE_MODEL_LABELS = { - "claude-sonnet-4-20250514": "Sonnet 4", - "claude-opus-4-20250514": "Opus 4", + "claude-sonnet-5": "Sonnet 5", + "claude-opus-4-8": "Opus 4.8", + "claude-sonnet-4-6": "Sonnet 4.6", + "claude-sonnet-4-5": "Sonnet 4.5", "claude-3-7-sonnet-latest": "Sonnet 3.7", "claude-3-5-sonnet-latest": "Sonnet 3.5", - "claude-3-5-haiku-latest": "Haiku 3.5", + "claude-haiku-4-5": "Haiku 4.5", +} + +RETIRED_CLAUDE_MODEL_ALIASES = { + "claude-sonnet-4-20250514": "claude-sonnet-5", + "claude-opus-4-20250514": "claude-opus-4-8", } @@ -1530,10 +1588,20 @@ def llm_model_label(model_id: str, provider: str) -> str: DEFAULT_LLM_MODEL = { "openai": "gpt-5-mini", "gemini": "gemini-2.5-flash", - "claude": "claude-sonnet-4-20250514", + "claude": "claude-sonnet-5", } +def resolve_llm_model(model_id: str, provider: str) -> str: + model_id = s(model_id) or DEFAULT_LLM_MODEL.get(provider, "gpt-5-mini") + if provider == "claude": + model_id = RETIRED_CLAUDE_MODEL_ALIASES.get(model_id, model_id) + available = llm_models_for_provider("claude") + if model_id not in available: + model_id = DEFAULT_LLM_MODEL.get("claude", available[0]) + return model_id + + def llm_models_for_provider(provider: str) -> list[str]: if provider == "gemini": return ALL_GEMINI_MODELS @@ -2901,9 +2969,10 @@ def sync_all_ui_fields_to_plan(*, refresh_editor: bool = False, force_editor: bo "domain_def": s(st.session_state.domain_def), "hydrological_model": current_hydrological_model(), "forcing_dataset": s(st.session_state.forcing_dataset), - "experiment_time_start": s(st.session_state.tstart), - "experiment_time_end": s(st.session_state.tend), } + plan_tstart, plan_tend = ui_experiment_dates_for_plan() + values["experiment_time_start"] = plan_tstart + values["experiment_time_end"] = plan_tend st.session_state.selected_pour_point = values["pour_point_coords"] st.session_state.selected_bounding_box = values["bounding_box_coords"] @@ -3643,7 +3712,50 @@ def render_map_layer_checkboxes(key_prefix: str) -> int: # Scrollable swatch list up to this many classes; above that use compact gradient summary. MAP_LEGEND_MAX_SWATCHES = 120 MAP_LEGEND_TWO_COLUMN_MIN = 10 -MAP_LEGEND_SCROLL_MAX_HEIGHT_PX = 240 +MAP_LEGEND_SCROLL_MAX_HEIGHT_PX = 140 +MAP_LEGEND_MIN_WIDTH_PX = 150 +MAP_LEGEND_MAX_WIDTH_PX = 190 + + +def _add_workflow_map_css(m: folium.Map) -> None: + """Keep map controls legible and compact inside narrow Streamlit columns.""" + from branca.element import Element + + m.get_root().header.add_child( + Element( + f""" + + """ + ) + ) def _pour_point_legend_icon_html() -> str: @@ -3981,10 +4093,9 @@ def _add_choropleth_legend_to_map( template = Template( """ {% macro html(this, kwargs) %} -