From b924a623931cc392b4c06e0f4a0d7d53e749caa7 Mon Sep 17 00:00:00 2001 From: sandeshit Date: Mon, 3 Aug 2026 15:00:19 +0545 Subject: [PATCH 1/6] feat(summary): add summarization fields for imminent type. --- dref/summary.py | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/dref/summary.py b/dref/summary.py index 4976ddece..73c18133d 100644 --- a/dref/summary.py +++ b/dref/summary.py @@ -18,10 +18,8 @@ ENCODING_NAME = "cl100k_base" -MAX_OUTPUT_CHARS_PER_FIELD = 1500 MAX_INPUT_TOKENS = 10000 - # The models a DrefSummary can be generated from. DrefSummarySource = Union[Dref, DrefOperationalUpdate, DrefFinalReport] @@ -44,7 +42,8 @@ "You are an IFRC expert analyst specializing in DREF (Disaster Response Emergency Fund) " "operations. Analyze the provided DREF data and produce clear, professional humanitarian " "summaries suitable for IFRC staff and National Society personnel. Use only the information " - "provided in the data; do not invent facts, figures, or details." + "provided in the data; do not invent facts, figures, or details. Where supporting information " + "for a section is absent, return an empty string for that section rather than speculating." ) # Section prompt builders @@ -90,7 +89,7 @@ def _build_lessons_learned_prompt(**kwargs) -> str: } GLOBAL_PROMPT = ( - "The DREF data above is organised by summary section. Using ONLY that data, write five concise " + "The DREF data above is organised by summary section. Using ONLY that data, write five " "summary sections. Return a single JSON object (and nothing else) with exactly these keys, each " "summarising the block of the same name:\n" "\n" @@ -109,8 +108,15 @@ def _build_lessons_learned_prompt(**kwargs) -> str: "- Summarise only what each section's data actually contains; do not add topics or details it " "does not mention.\n" "- Preserve every specific figure, location and timeframe from the source; never fabricate them.\n" - "- Each value must be plain text (no markdown, no bullet lists, no nested JSON): one " - "well-structured paragraph in professional humanitarian language.\n" + "- If a section's data block is empty or holds no usable content, set that key to an empty " + "string. Never write a sentence stating that data is missing, not provided or not recorded.\n" + "- Scale each section's length to how much its source data actually contains: a brief source " + "yields a single short paragraph; a detailed source yields two to three full paragraphs. Never " + "pad a thin section to reach a length, and never compress a detailed one into a single " + "paragraph — a long source deserves a correspondingly long summary.\n" + "- Each value must be plain text (no markdown, no bullet lists, no nested JSON): one or more " + "well-structured paragraphs in professional humanitarian language, separated by a blank line, " + "or an empty string when that section has no data.\n" "- Return ONLY the JSON object, with no surrounding prose or code fences." ) @@ -138,6 +144,9 @@ def _extract_fields(obj, field_names: List[str]) -> dict: SITUATIONAL_COMMON_FIELDS: List[str] = ["event_description", "event_scope"] +# Imminent DREF applications created on the v2 use hazard_date_and_location. +IMMINENT_SITUATIONAL_FIELDS: List[str] = ["hazard_date_and_location"] + OPERATIONAL_COMMON_FIELDS: List[str] = ["operation_objective", "response_strategy"] PEOPLE_COMMON_FIELDS: List[str] = ["people_assisted", "selection_criteria"] @@ -151,14 +160,10 @@ def __init__(self): @staticmethod def _situational_overview_kwargs(source_doc) -> dict: - """Build situational_overview kwargs — common across all document types. - - ``event_scope`` is one of the common fields; when it is empty (e.g. an - Imminent DREF Application where the scope is not yet known) - ``_extract_fields`` drops it automatically, while by the Final Report - stage the event has materialized and the field feeds the summary. - """ - return _extract_fields(source_doc, SITUATIONAL_COMMON_FIELDS) + """Imminent v2 applications describe the situation in the scenario analysis fields; others use the common ones.""" + if isinstance(source_doc, Dref) and source_doc.type_of_dref == Dref.DrefType.IMMINENT and source_doc.is_dref_imminent_v2: + return _extract_fields(source_doc, IMMINENT_SITUATIONAL_FIELDS) + return _extract_fields(source_doc, SITUATIONAL_COMMON_FIELDS) # event_scope is empty for Assessment; dropped @staticmethod def _challenges_and_lessons_kwargs(source_doc) -> Dict[str, dict]: @@ -345,8 +350,5 @@ def generate_all(self, source_doc: DrefSummarySource, section_kwargs: Optional[D value = parsed.get(field_name) if not isinstance(value, str): continue - summary = value.strip() - if len(summary) > MAX_OUTPUT_CHARS_PER_FIELD: - summary = summary[:MAX_OUTPUT_CHARS_PER_FIELD].rstrip() - results[field_name] = summary + results[field_name] = value.strip() return results From 98d136c05bece492d423c39b07938fd3019c2898 Mon Sep 17 00:00:00 2001 From: sandeshit Date: Mon, 3 Aug 2026 15:00:59 +0545 Subject: [PATCH 2/6] feat(llm): alter the api version for azure-openai. --- main/llm.py | 2 +- main/settings.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/main/llm.py b/main/llm.py index 46860e10a..5c3b82ae4 100644 --- a/main/llm.py +++ b/main/llm.py @@ -55,7 +55,7 @@ def client(self): return AzureOpenAI( azure_endpoint=settings.AZURE_OPENAI_ENDPOINT, api_key=settings.AZURE_OPENAI_API_KEY, - api_version="2023-05-15", + api_version=settings.AZURE_OPENAI_API_VERSION, ) def get_response(self, messages: Messages) -> Optional[str]: diff --git a/main/settings.py b/main/settings.py index b7b36d5f7..0f8c4d1c9 100644 --- a/main/settings.py +++ b/main/settings.py @@ -148,6 +148,9 @@ AZURE_OPENAI_ENDPOINT=(str, None), AZURE_OPENAI_API_KEY=(str, None), AZURE_OPENAI_DEPLOYMENT_NAME=(str, None), + # Azure OpenAI REST api-version. Keep this on a GA (non-preview) release; a + # deployment can be pinned back to an older one without a code change. + AZURE_OPENAI_API_VERSION=(str, "2024-10-21"), # Use a fake LLM client instead of calling Azure OpenAI USE_DUMMY_LLM_CLIENT=(bool, False), # ReliefWeb appname @@ -907,6 +910,7 @@ def decode_base64(env_key, fallback_env_key): AZURE_OPENAI_ENDPOINT = env("AZURE_OPENAI_ENDPOINT") AZURE_OPENAI_API_KEY = env("AZURE_OPENAI_API_KEY") AZURE_OPENAI_DEPLOYMENT_NAME = env("AZURE_OPENAI_DEPLOYMENT_NAME") +AZURE_OPENAI_API_VERSION = env("AZURE_OPENAI_API_VERSION") USE_DUMMY_LLM_CLIENT = env("USE_DUMMY_LLM_CLIENT") OIDC_ENABLE = env("OIDC_ENABLE") From 77c280cd6a4d3e773a07eff772cafd2fdefc843b Mon Sep 17 00:00:00 2001 From: sandeshit Date: Wed, 5 Aug 2026 14:04:21 +0545 Subject: [PATCH 3/6] fixup! feat(llm): alter the api version for azure-openai. --- dref/summary.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dref/summary.py b/dref/summary.py index 73c18133d..9dbe88940 100644 --- a/dref/summary.py +++ b/dref/summary.py @@ -110,8 +110,7 @@ def _build_lessons_learned_prompt(**kwargs) -> str: "- Preserve every specific figure, location and timeframe from the source; never fabricate them.\n" "- If a section's data block is empty or holds no usable content, set that key to an empty " "string. Never write a sentence stating that data is missing, not provided or not recorded.\n" - "- Scale each section's length to how much its source data actually contains: a brief source " - "yields a single short paragraph; a detailed source yields two to three full paragraphs. Never " + "- Scale each section's length to how much its source data actually contains. Never " "pad a thin section to reach a length, and never compress a detailed one into a single " "paragraph — a long source deserves a correspondingly long summary.\n" "- Each value must be plain text (no markdown, no bullet lists, no nested JSON): one or more " From 6a4ca6b62ca60ef2b4180ff9fc8008a3a49bbe3b Mon Sep 17 00:00:00 2001 From: sandeshit Date: Tue, 11 Aug 2026 14:49:29 +0545 Subject: [PATCH 4/6] fixup! feat(llm): alter the api version for azure-openai. --- dref/summary.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/dref/summary.py b/dref/summary.py index 9dbe88940..49978c89d 100644 --- a/dref/summary.py +++ b/dref/summary.py @@ -40,8 +40,9 @@ SYSTEM_MESSAGE = ( "You are an IFRC expert analyst specializing in DREF (Disaster Response Emergency Fund) " - "operations. Analyze the provided DREF data and produce clear, professional humanitarian " - "summaries suitable for IFRC staff and National Society personnel. Use only the information " + "operations. You write short executive summaries for IFRC staff and National Society personnel " + "who will read the full document separately, so your task is to condense and synthesise, never " + "to restate the source. Use only the information " "provided in the data; do not invent facts, figures, or details. Where supporting information " "for a section is absent, return an empty string for that section rather than speculating." ) @@ -90,8 +91,8 @@ def _build_lessons_learned_prompt(**kwargs) -> str: GLOBAL_PROMPT = ( "The DREF data above is organised by summary section. Using ONLY that data, write five " - "summary sections. Return a single JSON object (and nothing else) with exactly these keys, each " - "summarising the block of the same name:\n" + "condensed summary sections. Return a single JSON object (and nothing else) with exactly these " + "keys, each summarising the block of the same name:\n" "\n" ' "situational_overview": The disaster situation and the rationale for the operation. Use the ' 'data under the "situational_overview" key.\n' @@ -107,13 +108,17 @@ def _build_lessons_learned_prompt(**kwargs) -> str: "Requirements:\n" "- Summarise only what each section's data actually contains; do not add topics or details it " "does not mention.\n" - "- Preserve every specific figure, location and timeframe from the source; never fabricate them.\n" + "- Synthesise, do not concatenate: group related points into a coherent narrative instead of " + "restating the source line by line or field by field. Merge repeated or overlapping points into " + "a single statement.\n" + "- Each section is at most three paragraphs, whatever the length of its source data. A thin " + "source should yield a single paragraph; a rich one may use the full three, but no source " + "justifies more.\n" + "- Open each section with its single most important point, then add supporting context.\n" + "- Preserve important facts and figures exactly as given; never invent or alter them.\n" "- If a section's data block is empty or holds no usable content, set that key to an empty " "string. Never write a sentence stating that data is missing, not provided or not recorded.\n" - "- Scale each section's length to how much its source data actually contains. Never " - "pad a thin section to reach a length, and never compress a detailed one into a single " - "paragraph — a long source deserves a correspondingly long summary.\n" - "- Each value must be plain text (no markdown, no bullet lists, no nested JSON): one or more " + "- Each value must be plain text (no markdown, no bullet lists, no nested JSON): one to three " "well-structured paragraphs in professional humanitarian language, separated by a blank line, " "or an empty string when that section has no data.\n" "- Return ONLY the JSON object, with no surrounding prose or code fences." From aecfeb56964970359c95a046d9590362d74866f7 Mon Sep 17 00:00:00 2001 From: sandeshit Date: Thu, 13 Aug 2026 12:07:01 +0545 Subject: [PATCH 5/6] fixup! feat(llm): alter the api version for azure-openai. --- dref/summary.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/dref/summary.py b/dref/summary.py index 49978c89d..4c10a8c41 100644 --- a/dref/summary.py +++ b/dref/summary.py @@ -57,7 +57,10 @@ def _section_data_json(kwargs: dict) -> str: def _build_situational_overview_prompt(**kwargs) -> str: data_json = _section_data_json(kwargs) - return f'Data for "situational_overview" — the disaster situation and rationale for the operation:\n{data_json}' + return ( + f'Data for "situational_overview" — the disaster situation and rationale for the operation, ' + f"and the scale and effects of the event:\n{data_json}" + ) def _build_operational_strategy_prompt(**kwargs) -> str: @@ -94,8 +97,8 @@ def _build_lessons_learned_prompt(**kwargs) -> str: "condensed summary sections. Return a single JSON object (and nothing else) with exactly these " "keys, each summarising the block of the same name:\n" "\n" - ' "situational_overview": The disaster situation and the rationale for the operation. Use the ' - 'data under the "situational_overview" key.\n' + ' "situational_overview": The disaster situation and the rationale for the operation, and the ' + 'scale and effects of the event. Use the data under the "situational_overview" key.\n' ' "operational_strategy": The overall objective and strategic approach of the response. Use the ' 'data under the "operational_strategy" key.\n' ' "people_centered_approach": Who is targeted and how they are selected and engaged. Use the ' From bdfc395ff7a47e2294431a563e047b6153a6a591 Mon Sep 17 00:00:00 2001 From: sandeshit Date: Thu, 13 Aug 2026 14:28:12 +0545 Subject: [PATCH 6/6] fixup! feat(llm): alter the api version for azure-openai. --- dref/summary.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/dref/summary.py b/dref/summary.py index 4c10a8c41..292ff4a87 100644 --- a/dref/summary.py +++ b/dref/summary.py @@ -40,7 +40,7 @@ SYSTEM_MESSAGE = ( "You are an IFRC expert analyst specializing in DREF (Disaster Response Emergency Fund) " - "operations. You write short executive summaries for IFRC staff and National Society personnel " + "operations. You write executive summaries for IFRC staff and National Society personnel " "who will read the full document separately, so your task is to condense and synthesise, never " "to restate the source. Use only the information " "provided in the data; do not invent facts, figures, or details. Where supporting information " @@ -57,10 +57,7 @@ def _section_data_json(kwargs: dict) -> str: def _build_situational_overview_prompt(**kwargs) -> str: data_json = _section_data_json(kwargs) - return ( - f'Data for "situational_overview" — the disaster situation and rationale for the operation, ' - f"and the scale and effects of the event:\n{data_json}" - ) + return f'Data for "situational_overview" — the disaster situation and rationale for the operation:\n{data_json}' def _build_operational_strategy_prompt(**kwargs) -> str: @@ -94,11 +91,11 @@ def _build_lessons_learned_prompt(**kwargs) -> str: GLOBAL_PROMPT = ( "The DREF data above is organised by summary section. Using ONLY that data, write five " - "condensed summary sections. Return a single JSON object (and nothing else) with exactly these " + "summary sections. Return a single JSON object (and nothing else) with exactly these " "keys, each summarising the block of the same name:\n" "\n" - ' "situational_overview": The disaster situation and the rationale for the operation, and the ' - 'scale and effects of the event. Use the data under the "situational_overview" key.\n' + ' "situational_overview": The disaster situation and the rationale for the operation. Use the ' + 'data under the "situational_overview" key.\n' ' "operational_strategy": The overall objective and strategic approach of the response. Use the ' 'data under the "operational_strategy" key.\n' ' "people_centered_approach": Who is targeted and how they are selected and engaged. Use the ' @@ -114,14 +111,15 @@ def _build_lessons_learned_prompt(**kwargs) -> str: "- Synthesise, do not concatenate: group related points into a coherent narrative instead of " "restating the source line by line or field by field. Merge repeated or overlapping points into " "a single statement.\n" - "- Each section is at most three paragraphs, whatever the length of its source data. A thin " - "source should yield a single paragraph; a rich one may use the full three, but no source " - "justifies more.\n" + "- Let each section's length follow its source: a thin source yields a single short paragraph, " + "a substantial one several. Never pad a thin section to reach a length.\n" + "- Give proportionate treatment to every field in the section's data block. Do not spend the " + "section on the first field and leave later fields unaddressed.\n" "- Open each section with its single most important point, then add supporting context.\n" "- Preserve important facts and figures exactly as given; never invent or alter them.\n" "- If a section's data block is empty or holds no usable content, set that key to an empty " "string. Never write a sentence stating that data is missing, not provided or not recorded.\n" - "- Each value must be plain text (no markdown, no bullet lists, no nested JSON): one to three " + "- Each value must be plain text (no markdown, no bullet lists, no nested JSON): one or more " "well-structured paragraphs in professional humanitarian language, separated by a blank line, " "or an empty string when that section has no data.\n" "- Return ONLY the JSON object, with no surrounding prose or code fences."