diff --git a/dref/summary.py b/dref/summary.py index 4976ddece..9dbe88940 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,14 @@ 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. 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 +143,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 +159,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 +349,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 0927e30ef..63344657d 100644 --- a/main/settings.py +++ b/main/settings.py @@ -146,6 +146,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 @@ -870,6 +873,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")