diff --git a/dref/summary.py b/dref/summary.py index 4976ddece..292ff4a87 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] @@ -42,9 +40,11 @@ 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 " - "provided in the data; do not invent facts, figures, or details." + "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 " + "for a section is absent, return an empty string for that section rather than speculating." ) # Section prompt builders @@ -90,9 +90,9 @@ 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 " - "summary sections. Return a single JSON object (and nothing else) with exactly these keys, each " - "summarising the block of the same name:\n" + "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" ' "situational_overview": The disaster situation and the rationale for the operation. Use the ' 'data under the "situational_overview" key.\n' @@ -108,9 +108,20 @@ 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" - "- Each value must be plain text (no markdown, no bullet lists, no nested JSON): one " - "well-structured paragraph in professional humanitarian language.\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" + "- 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 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 +149,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 +165,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 +355,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 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")