Spaces:
Running
Running
| # app.py | |
| # ============================================================================= | |
| # THE NEW MILITARY DECISION MAKING PROCESS (5D-MDMP) - FULL APP | |
| # ============================================================================= | |
| # Author: Assembled for Keshav Mazumdar | |
| # Purpose: Single-file Hugging Face Space (Gradio) implementing 5D MDMP, | |
| # Integrated Warfare MDMP addon (SPTR, CARVER, BOS, Attack on Intent), | |
| # Knowledge base links + inline Markdown full document, | |
| # SITREP loader, Indian Army-style SA, COA(BULL) with dynamic triggers. | |
| # ============================================================================= | |
| import os | |
| import json | |
| import datetime | |
| import logging | |
| import re | |
| from collections import defaultdict | |
| from difflib import SequenceMatcher | |
| from pathlib import Path | |
| import gradio as gr | |
| import matplotlib.pyplot as plt | |
| import numpy as np | |
| # ------------------------- | |
| # Logging | |
| # ------------------------- | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger("5D-MDMP") | |
| # ------------------------- | |
| # Banner / Title / Intro | |
| # ------------------------- | |
| BANNER_URL = "https://huggingface.co/spaces/Militaryint/mdmp/resolve/main/banner.png" | |
| TITLE = "THE NEW MILITARY DECISION MAKING PROCESS" | |
| INTRO = ( | |
| "A new military decision making process by Keshav Mazumdar \n" | |
| "(To avert enemy surprise who may use deception in indicators to effect wrong situational awareness of friendly forces)\n\n" | |
| "This MDMP uses the 5D System (Detect, Deny, Deter, Deliver, Destroy).\n\n" | |
| "- At the beginning of MDMP: Observed enemy data (size, activity, location, unit type, equipment) is examined through the 5D lens to understand enemy intent.\n" | |
| "- At COA planning: Reverse 5D is applied — for each enemy COA, friendly forces plan corresponding 5D actions." | |
| ) | |
| # ------------------------- | |
| # Full Exhaustive Questionnaire (uncut sample) | |
| # ------------------------- | |
| QUESTIONNAIRE = """ | |
| A) INITIAL SITUATIONAL AWARENESS — SEE THE ENEMY (5D LENS) | |
| (note: for each D, capture indicators, sources, timestamps, geolocation, direction of movement, and supporting evidence) | |
| 1) DETECT — Is the enemy attempting to detect our forces, intentions, vulnerabilities, or movements? | |
| - Evidence checklist: | |
| - Are there observed reconnaissance assets? (UAVs, scouts, observation posts, long-range optics) | |
| - Is there SIGINT indicative of forward listening/ELINT? (new or atypical radio chatter, beaconing patterns) | |
| - Are local informants reporting suspicious surveillance or 'spotters'? | |
| - Are there repeated sensor contacts at the same time of day (pattern)? Provide timestamps. | |
| - Is there metadata anomaly (e.g., sudden new cell tower associations, unusual comm routing)? | |
| - Are supply/logistics flows changing that would support detection (e.g., increased spare parts for optics)? | |
| - Specific questions to answer: | |
| - What sensors/platforms is the enemy using to observe (type, approximate range)? | |
| - Where (grid/lat-lon) are those assets located relative to our positions? | |
| - When did the detection activity start? Frequency? (daily, nightly) | |
| - Source reliability & provenance for each observation (HUMINT id, SIGINT tag, imagery timestamp). | |
| - Deception checks: Could these be decoys intended to lure our observation or choke ISR? | |
| 2) DENY — Is the enemy taking measures to deny us information or access to their activities? | |
| - Evidence checklist: | |
| - Electronic countermeasures (jamming, frequency hopping, signal masking). | |
| - Camouflage/concealment of positions (smoke, netting, movement at night, use of tunnels). | |
| - False logistics & decoys (dummy vehicles, false camps). | |
| - OPSEC behavior: deliberate silence, message deletion, use of couriers. | |
| - Disruption of our ISR (UAV interference, GPS spoofing). | |
| - Specific questions: | |
| - What methods are observed that would reduce our intelligence collection? (list exact times/locations) | |
| - Which of our sensors are affected — imagery, SIGINT, HUMINT reliability? | |
| - Are there indications the enemy knows our ISR collection windows (suggesting prior detection)? | |
| - What gaps in data exist that may indicate successful denial (missing traffic, sudden disappearance)? | |
| - Can we attribute denial to a particular enemy unit or capability? | |
| 3) DETER — Is the enemy attempting to deter our actions or create a deterrent climate? | |
| - Evidence checklist: | |
| - Demonstrations of force: visible troop movements, convoys, checkpoints, fortified positions. | |
| - PSYOPS/propaganda, threats or warnings broadcast to locals or forces. | |
| - Increased patrols, ambush posture, booby-traps visible on routes. | |
| - Repetitive shows-of-force timed to our movements. | |
| - Specific questions: | |
| - Is there evidence of posture intended to intimidate (fortifications, large-calibre weapons on display)? | |
| - Are there visible escort forces or overlapping fields of fire that deter movement? | |
| - Are local civilians being told to avoid cooperation with security forces? | |
| - How might the enemy's deterrence change our options or freedom of movement? | |
| 4) DELIVER — Is the enemy trying to deliver/secure local population support or remove locals from our influence? | |
| - Evidence checklist: | |
| - Civic actions by enemy or allied local groups (food distribution, payments, protection pledges). | |
| - Coercion: threats, targeted killings of pro-government actors, intimidation of local leaders. | |
| - Building of influence networks: recruiters, propaganda, local committees under enemy control. | |
| - Offers of ‘protection’ in areas our forces patrol (suggests attempt to remove locals from our influence). | |
| - Specific questions: | |
| - Who among the local population is being targeted? (leaders, merchants, clinics) | |
| - Are there new or increased local grievances being exploited by enemy messaging? | |
| - Do we have HUMINT on recruitment or local collaboration? Provide names/locations if available. | |
| - Could the enemy's influence operations impact force protection (informants, lookouts, IED assistance)? | |
| 5) DESTROY — Is there evidence of preparations to physically destroy our forces, bases, or infrastructure? | |
| - Evidence checklist: | |
| - Movement of indirect fire systems, heavy weapons, IED materials, ambush signatures. | |
| - Targeting data: observers, range-finding activity, rehearsals, dry-runs on routes. | |
| - Logistics indicating offensive intent (fuel/munitions build-up, movement of assault teams at night). | |
| - Known enemy capabilities to strike (mortars, artillery, rockets, anti-armour). | |
| - Specific questions: | |
| - Are there confirmed munitions or weapon caches accessible to the observed unit? | |
| - What are the likely targets (columns, bases, infrastructure)? | |
| - Has there been rehearsal or practice attacks observed? Supply dumps? Night movement? | |
| - Timelines: expected attack window, likely axes of approach, my force vulnerabilities exposed. | |
| 6) CROSS-D DIAGNOSTICS & DECEPTION HUNTS | |
| - For every observation: check for contradictions between D indicators (e.g., high Detected + high Deny may indicate complex deception). | |
| - Deception Checklist: | |
| - Source redundancy? Are at least two independent sources confirming the same fact? | |
| - Temporal plausibility: do timestamps match known movement timelines? | |
| - Logistics match: does fuel/food/resupply support claimed enemy posture? | |
| - Communication metadata anomalies: improbable routing, timestamp shifts, reused identifiers. | |
| - Red-team hypotheses: what would the enemy want us to believe vs real intent? | |
| - Specific questions: | |
| - Which indicators, if false, would most change your hypothesis? (list critical unknowns) | |
| - Which ISR tasks would best discriminate between alternate hypotheses? | |
| 7) OBSERVATION METADATA (always capture) | |
| - Observation ID, timestamp (UTC), observer/source type, source reliability (0-1), lat-lon / grid, raw text, attachments (image/sigint clip). | |
| - Note any potential biases in the source (local informant under threat, single-sensor detection). | |
| --------------------------------------------------------------------- | |
| B) COA PLANNING — REVERSE 5D (For each enemy hypothesis, plan friendly action) | |
| (For each hypothesis Hn generated in the SA step answer the following per D; fill in triggers, resources, sequencing, and BDA metrics) | |
| ... (The full questionnaire can be viewed in the Integrated MDMP document) | |
| """ | |
| # ------------------------- | |
| # Knowledge Base (links) | |
| # ------------------------- | |
| KNOWLEDGE_BASE = { | |
| "Flowchart (PDF)": "knowledge_base/flowchart.pdf", | |
| "Checklist (PDF)": "knowledge_base/checklist.pdf", | |
| "Sample Dataset (JSON)": "knowledge_base/sample_sitreps.json", | |
| "Target Folder Template (JSON)": "knowledge_base/target_folder_template.json", | |
| "Integrated MDMP (MD)": "knowledge_base/integrated_mdmp_detailed.md", | |
| } | |
| # Ensure knowledge_base folder exists (runtime workspace) | |
| Path("knowledge_base").mkdir(parents=True, exist_ok=True) | |
| # ------------------------- | |
| # Enhanced Keyword Bank | |
| # ------------------------- | |
| KEYWORDS = { | |
| "Detect": [ | |
| "scout", "recon", "reconnaissance", "observe", "observation", "surveil", "surveillance", | |
| "spot", "uav", "uavs", "drone", "drones", "sighting", "watch", "eyes-on", "spotter", | |
| "listening", "elint", "sigint", "listening-post" | |
| ], | |
| "Deny": [ | |
| "jam", "jamming", "opsec", "block", "scramble", "spoof", "spoofer", "hide", "mask", | |
| "camouflage", "decoy", "decoys", "dummy", "gps-spoof", "emcon" | |
| ], | |
| "Deter": [ | |
| "patrol", "show", "show-of-force", "posture", "harden", "checkpoint", "presence", | |
| "escort", "route security", "guard", "fortify", "fortified", "force protection" | |
| ], | |
| "Deliver": [ | |
| "liberate", "support", "influence", "population", "protect", "recruit", "bribe", "payment", | |
| "civic", "civic action", "hearts and minds", "aid", "protection", "community engagement", | |
| "coercion", "intimidate" | |
| ], | |
| "Destroy": [ | |
| "attack", "strike", "kill", "eliminate", "ambush", "ambushed", "ied", "bomb", "mortar", | |
| "rocket", "artillery", "raid", "assault", "fire", "indirect-fire", "weapons cache", "cache" | |
| ], | |
| } | |
| # ------------------------- | |
| # Utility: fuzzy token similarity | |
| # ------------------------- | |
| def similar(a, b): | |
| return SequenceMatcher(None, a, b).ratio() | |
| # ------------------------- | |
| # Fuzzy scoring + evidence extraction | |
| # ------------------------- | |
| def fuzzy_score_with_evidence(text, keywords, token_threshold=0.85): | |
| text_low = text.lower() | |
| tokens = re.findall(r"\w+|\S", text_low) | |
| sentences = re.split(r'(?<=[.!?])\s+', text) | |
| scores = {k: 0 for k in keywords} | |
| evidence = {k: [] for k in keywords} | |
| for d, words in keywords.items(): | |
| for w in words: | |
| w_low = w.lower() | |
| for i, t in enumerate(tokens): | |
| # direct substring match or fuzzy similarity on token | |
| try: | |
| if w_low in t or similar(w_low, t) >= token_threshold: | |
| scores[d] += 1 | |
| # find sentence containing token for evidence | |
| sent = next((s for s in sentences if t in s.lower()), text) | |
| evidence[d].append({"word": w, "token": t, "index": i, "sentence": sent}) | |
| except Exception: | |
| continue | |
| return scores, evidence | |
| # ------------------------- | |
| # OpenAI client detection (modern/classic) | |
| # ------------------------- | |
| try: | |
| from openai import OpenAI as ModernOpenAI | |
| OPENAI_MODERN_AVAILABLE = True | |
| except Exception: | |
| ModernOpenAI = None | |
| OPENAI_MODERN_AVAILABLE = False | |
| try: | |
| import openai as openai_classic | |
| OPENAI_CLASSIC_AVAILABLE = True | |
| except Exception: | |
| openai_classic = None | |
| OPENAI_CLASSIC_AVAILABLE = False | |
| def get_openai_client(): | |
| key = os.environ.get("OPENAI_API_KEY") | |
| if not key: | |
| return None, "OPENAI_API_KEY missing" | |
| # Try modern | |
| if OPENAI_MODERN_AVAILABLE: | |
| try: | |
| client = ModernOpenAI(api_key=key) | |
| return (client, "modern") | |
| except Exception as e: | |
| logger.warning(f"Modern OpenAI init failed: {e}") | |
| if OPENAI_CLASSIC_AVAILABLE: | |
| try: | |
| openai_classic.api_key = key | |
| return (openai_classic, "classic") | |
| except Exception as e: | |
| logger.warning(f"Classic OpenAI init failed: {e}") | |
| return None, "No usable OpenAI client available" | |
| # ------------------------- | |
| # LLM self-score (asks model to rate 0..5 per D) | |
| # ------------------------- | |
| LLM_SELF_SCORE_INSTRUCTIONS = """ | |
| You are an analyst. Return EXACTLY a JSON object with keys detect, deny, deter, deliver, destroy | |
| and integer values 0..5 based solely on the SITREP text that follows. | |
| SITREP: | |
| --- | |
| {text} | |
| --- | |
| """ | |
| def llm_self_score(sitrep_text): | |
| client_info = get_openai_client() | |
| client, method = client_info if isinstance(client_info, tuple) else (None, "none") | |
| if client is None: | |
| return None, "No OpenAI client" | |
| prompt = LLM_SELF_SCORE_INSTRUCTIONS.format(text=sitrep_text) | |
| try: | |
| if method == "modern": | |
| resp = client.responses.create(model=os.environ.get("OPENAI_API_MODEL", "gpt-4o-mini"), | |
| input=prompt, max_output_tokens=200) | |
| out_text = "" | |
| for item in getattr(resp, "output", []) or []: | |
| if isinstance(item, dict): | |
| for c in item.get("content", []): | |
| if c.get("type") == "output_text": | |
| out_text += c.get("text", "") | |
| else: | |
| chat = client.ChatCompletion.create(model=os.environ.get("OPENAI_API_MODEL", "gpt-3.5-turbo"), | |
| messages=[{"role":"user","content":prompt}], max_tokens=200) | |
| out_text = chat.choices[0].message.content.strip() | |
| # extract JSON object | |
| start = out_text.find("{") | |
| end = out_text.rfind("}") | |
| if start != -1 and end != -1: | |
| partial = out_text[start:end+1] | |
| parsed = json.loads(partial) | |
| normalized = { | |
| "Detect": int(parsed.get("detect", 0)), | |
| "Deny": int(parsed.get("deny", 0)), | |
| "Deter": int(parsed.get("deter", 0)), | |
| "Deliver": int(parsed.get("deliver", 0)), | |
| "Destroy": int(parsed.get("destroy", 0)), | |
| } | |
| return normalized, None | |
| return None, f"No JSON in LLM output: {out_text}" | |
| except Exception as e: | |
| logger.exception("llm_self_score failure") | |
| return None, str(e) | |
| # ------------------------- | |
| # Merge and normalize scores (0..5) | |
| # ------------------------- | |
| def merge_and_normalize_scores(keyword_scores, llm_scores=None, kw_weight=0.6, llm_weight=0.4, scale_to=5): | |
| max_kw = max(keyword_scores.values()) if keyword_scores and max(keyword_scores.values()) > 0 else 1 | |
| normalized_kw = {k: (v / max_kw) * scale_to for k, v in keyword_scores.items()} | |
| if llm_scores: | |
| normalized_llm = {k: float(llm_scores.get(k, 0)) for k in keyword_scores.keys()} | |
| else: | |
| normalized_llm = {k: 0.0 for k in keyword_scores.keys()} | |
| merged = {} | |
| for k in keyword_scores.keys(): | |
| merged_val = (normalized_kw.get(k, 0) * kw_weight) + (normalized_llm.get(k, 0) * llm_weight) | |
| merged[k] = round(max(0.0, min(scale_to, merged_val)), 2) | |
| return merged | |
| # ------------------------- | |
| # Cross-diagnostics (deception hints) | |
| # ------------------------- | |
| def cross_diagnostics(evidence, merged_scores): | |
| flags = [] | |
| try: | |
| if len(evidence.get("Detect", [])) >= 2 and len(evidence.get("Deny", [])) >= 2: | |
| flags.append("High Detect & high Deny concurrently — potential deception; require source redundancy.") | |
| # If Detect high but Destroy low -> possible recon/decoy | |
| if merged_scores.get("Detect", 0) >= 3 and merged_scores.get("Destroy", 0) <= 1: | |
| flags.append("Detect HIGH but Destroy LOW — possible recon/decoy activity or feint.") | |
| # Deliver presence with low Destroy -> population influence move | |
| if merged_scores.get("Deliver", 0) >= 3 and merged_scores.get("Destroy", 0) <= 1: | |
| flags.append("Deliver HIGH & Destroy LOW — focused influence ops; watch for lookouts and informants.") | |
| # Check evidence sentence redundancy | |
| for d, evlist in evidence.items(): | |
| distinct = len(set([e.get("sentence", "").strip() for e in evlist if e.get("sentence")])) | |
| if len(evlist) > 1 and distinct < 2: | |
| flags.append(f"{d.upper()}: multiple token matches but low sentence redundancy -> low source diversity.") | |
| except Exception: | |
| pass | |
| return flags | |
| # ------------------------- | |
| # Radar chart helper (0..5) | |
| # ------------------------- | |
| def make_radar_chart_from_merged(merged_scores): | |
| labels = list(merged_scores.keys()) | |
| values = [float(merged_scores.get(k, 0)) for k in labels] | |
| values += values[:1] | |
| angles = np.linspace(0, 2 * np.pi, len(labels), endpoint=False).tolist() | |
| angles += angles[:1] | |
| fig, ax = plt.subplots(figsize=(5,5), subplot_kw=dict(polar=True)) | |
| ax.plot(angles, values, linewidth=2) | |
| ax.fill(angles, values, alpha=0.25) | |
| ax.set_thetagrids(np.degrees(angles[:-1]), labels) | |
| ax.set_ylim(0, 5) | |
| for i, angle in enumerate(angles[:-1]): | |
| ax.text(angle, values[i] + 0.15, str(values[i]), horizontalalignment='center', verticalalignment='bottom', fontsize=9) | |
| ax.set_title("5D Spectrum (0-5 intensity scale)") | |
| return fig | |
| # ------------------------- | |
| # Fallback two-lens report generator (guaranteed output when LLM absent) | |
| # ------------------------- | |
| def fallback_two_lens_report(observation_json): | |
| merged = observation_json.get("merged_5d_scores", {}) | |
| evidence = observation_json.get("evidence", {}) | |
| cross_flags = observation_json.get("cross_diagnostics", []) | |
| raw = observation_json.get("raw_text", "") | |
| lines = [] | |
| lines.append("⚠️ FALLBACK TWO-LENS REPORT (LLM unavailable)") | |
| lines.append("\nA) INITIAL SITUATIONAL AWARENESS — ENEMY 5D VIEW") | |
| for d in ["Detect", "Deny", "Deter", "Deliver", "Destroy"]: | |
| val = merged.get(d, 0) | |
| lines.append(f"\n{d.upper()} (intensity {val}/5):") | |
| evs = evidence.get(d, []) | |
| if evs: | |
| for e in evs[:3]: | |
| sent = e.get("sentence", "").strip() | |
| lines.append(f"- Indicator: '{e.get('word')}' -> {sent[:200]}") | |
| else: | |
| lines.append("- No explicit textual indicators located for this D (INFERRED).") | |
| if d == "Detect" and merged.get("Detect",0) >= 3 and merged.get("Destroy",0) <= 1: | |
| lines.append("- NOTE: Strong detection activity but low Destroy — possible reconnaissance/feint.") | |
| lines.append("\nDECEPTION & CROSS DIAGNOSTICS:") | |
| if cross_flags: | |
| for f in cross_flags: | |
| lines.append(f"- {f}") | |
| else: | |
| lines.append("- No cross-diagnostic flags raised automatically; continue multi-source checks.") | |
| lines.append("\nSITUATION HYPOTHESES TABLE (AUTO-GENERATED - INFERENTIAL)") | |
| h1_desc = "" | |
| h2_desc = "" | |
| if merged.get("Destroy",0) >= 3 or merged.get("Detect",0) >= 3 and merged.get("Destroy",0) >= 2: | |
| h1_desc = "H1: Credible attack/ambush planned in stated area (evidence: Destroy indicators present; IED/wpn movement)." | |
| h2_desc = "H2: Secondary/feint or deception not excluded; confirm with ISR on logistics and supply lines." | |
| else: | |
| h1_desc = "H1: Recon/feint to draw attention (Detect high, Destroy low)." | |
| h2_desc = "H2: Influence/local control operation (Deliver high) to set up future capability." | |
| lines.append(f"H1 | {h1_desc} | Likelihood: 0.6") | |
| lines.append(f"H2 | {h2_desc} | Likelihood: 0.4") | |
| lines.append("\nCOA FAMILIES (REVERSE-5D) — Examples (Short):") | |
| lines.append("\nCOA H1-A: DEFENSIVE FP (Contain & Observe)") | |
| lines.append("- Mission: Maintain force protection; defeat ambush if occurs.") | |
| lines.append("- Detect: Increase persistent ISR (UAS night ops, continuous SIGINT sweep).") | |
| lines.append("- Deny: EMCON & comm discipline, randomize convoys, route clearance teams.") | |
| lines.append("- Deter: Visible mounted patrols & QRF on standby.") | |
| lines.append("- Deliver: Civil protection patrols to reduce local collab.") | |
| lines.append("- Destroy: Prepare target development for interdiction (only when positive ID and BDA expected).") | |
| lines.append("- Trigger: If Destroy indicators ≥ 3 OR ≥2 independent weapon cache confirmations -> escalate to interdiction COA.") | |
| lines.append("\nCOA H1-B: ACTIVE INTERDICTION (Limited Strike)") | |
| lines.append("- Mission: Prevent imminent ambush by targeted interdiction.") | |
| lines.append("- Detect: Rapid target confirmation with combined HUMINT+IMINT.") | |
| lines.append("- Deny: Isolate suspected area, block egress routes.") | |
| lines.append("- Deter: Use feint presence at nearby axes to split ENY attention.") | |
| lines.append("- Deliver: Protect local informants enabling arrest/capture.") | |
| lines.append("- Destroy: Time-sensitive precision strike/QRF removal of IED teams.") | |
| lines.append("- Trigger: If PIRs confirm weapons movement & lookouts -> execute.") | |
| lines.append("\nCOA H2-A: ISR & Deception Hunting (Low Level Probe)") | |
| lines.append("- Mission: Resolve ambiguity; do not escalate to kinetic action.") | |
| lines.append("- Detect: Controlled probes (small patrols, sensor baiting) to force ENY reaction.") | |
| lines.append("- Deny: Secure our ISR signatures to avoid revealing coverage.") | |
| lines.append("- Deter: Limited shows-of-force to deprioritize ENY confidence.") | |
| lines.append("- Deliver: Civil engagements to undercut ENY influence.") | |
| lines.append("- Destroy: None immediate; escalate only with robust confirmation.") | |
| lines.append("- Trigger: If ENY moves logistics or Destroy indicators rise -> escalate to H1 COAs.") | |
| lines.append("\nEXECUTIVE SUMMARY (Short):") | |
| lines.append("- Current auto-analysis suggests potential deception risk. Immediate action: prioritize ISR to remove ambiguity and cross-check HUMINT/SIGINT. Use conservative escalations with numeric triggers.") | |
| return "\n".join(lines) | |
| # ------------------------- | |
| # LLM prompt template for full two-lens report | |
| # ------------------------- | |
| LLM_PROMPT_TEMPLATE = """ | |
| SYSTEM: You are a military analyst assistant. ALWAYS produce TWO LENSES in the output: | |
| (A) INITIAL SITUATIONAL AWARENESS — Enemy 5D View | |
| (B) COA PLANNING — Reverse-5D | |
| INPUT OBSERVATION JSON: | |
| {observation_json} | |
| INSTRUCTIONS: | |
| 1) Write an "A) INITIAL SITUATIONAL AWARENESS — ENEMY 5D VIEW" section. Under each D (DETECT, DENY, DETER, DELIVER, DESTROY) list: | |
| - Indicators (bullet points) including sentence-level supporting evidence and the evidence index | |
| - Geo/time metadata from the observation (if missing, state MISSING) | |
| - Source reliability (0-1) | |
| - Deception checks and cross-D contradictions (explicit 'Yes/No' and why) | |
| 2) Produce a "Situation Hypotheses Table" with columns: | |
| Hypothesis ID | Short description | Likelihood (0–1) | Key 5D indicators | Confirming evidence | Disconfirming evidence | Required ISR | Time to test | |
| Provide at least 2 hypotheses (H1, H2). | |
| 3) For each hypothesis produce at least 2 COA families using REVERSE-5D with triggers, sequencing, logistics, and BDA metrics. | |
| 4) Output order: A) INITIAL SITUATIONAL AWARENESS, Situation Hypotheses Table, COA families, EXECUTIVE SUMMARY | |
| Produce the output as human-readable text suitable for commanders, with clear bullets and headings. If observation fields are missing, mark them as 'MISSING: <field>'. | |
| """ | |
| def llm_generate_full_report(observation_json): | |
| client_info = get_openai_client() | |
| client, method = client_info if isinstance(client_info, tuple) else (None, "none") | |
| if client is None: | |
| # Return fallback full two-lens report | |
| return fallback_two_lens_report(observation_json) | |
| prompt = LLM_PROMPT_TEMPLATE.format(observation_json=json.dumps(observation_json, indent=2)) | |
| try: | |
| if method == "modern": | |
| resp = client.responses.create(model=os.environ.get("OPENAI_API_MODEL", "gpt-4o-mini"), | |
| input=prompt, max_output_tokens=1500) | |
| out_text = "" | |
| for item in getattr(resp, "output", []) or []: | |
| if isinstance(item, dict): | |
| for c in item.get("content", []): | |
| if c.get("type") == "output_text": | |
| out_text += c.get("text", "") | |
| return out_text.strip() if out_text.strip() else fallback_two_lens_report(observation_json) | |
| else: | |
| chat = client.ChatCompletion.create(model=os.environ.get("OPENAI_API_MODEL", "gpt-3.5-turbo"), | |
| messages=[{"role":"user","content":prompt}], max_tokens=1500) | |
| out_text = chat.choices[0].message.content.strip() | |
| return out_text if out_text else fallback_two_lens_report(observation_json) | |
| except Exception as e: | |
| logger.exception("LLM generate full report failed") | |
| return fallback_two_lens_report(observation_json) | |
| # ------------------------- | |
| # Synthesize Indian Army style bulleted SA | |
| # ------------------------- | |
| def synthesize_bulleted_SA(observation_text, merged_scores, evidence, cross_flags): | |
| lines = [] | |
| # Header | |
| lines.append("SITUATIONAL AWARENESS (5D — INDIAN ARMY FORMAT)") | |
| # Short summary of observation | |
| summary = observation_text.strip().replace("\n", " ") | |
| lines.append(f"- ENY OBS (summary): {summary[:240]}{'...' if len(summary) > 240 else ''}") | |
| # 5D bullets | |
| for d in ["Detect", "Deny", "Deter", "Deliver", "Destroy"]: | |
| val = merged_scores.get(d, 0) | |
| evs = evidence.get(d, []) | |
| if evs: | |
| sents = [] | |
| for e in evs: | |
| s = e.get("sentence", "").strip() | |
| if s and s not in sents: | |
| sents.append(s) | |
| example = sents[0][:160] + "..." if sents else "See evidence entries." | |
| lines.append(f"- ENY {d.upper()}: Intensity {val}/5; Example: {example}") | |
| else: | |
| lines.append(f"- ENY {d.upper()}: Intensity {val}/5; No direct sentence-level indicator found (INFERRED).") | |
| # Deception flags | |
| lines.append("- DECEPTION / CROSS-DIAGNOSTICS:") | |
| if cross_flags: | |
| for f in cross_flags: | |
| lines.append(f" - {f}") | |
| else: | |
| lines.append(" - None automatically flagged. Recommend multi-source confirmation.") | |
| # Immediate action recommendations | |
| lines.append("- IMMEDIATE RECOMMENDED ACTIONS:") | |
| if merged_scores.get("Detect", 0) >= 2: | |
| lines.append(" - Prioritize persistent ISR (UAV night ops, SIGINT sweep), assign PIRs.") | |
| if merged_scores.get("Destroy", 0) >= 2: | |
| lines.append(" - Harden convoys and restrict movement on suspected routes; QRF on standby.") | |
| if merged_scores.get("Deliver", 0) >= 2: | |
| lines.append(" - Civilian protection measures and HUMINT expansion to remove collaborators.") | |
| if merged_scores.get("Deny", 0) >= 2: | |
| lines.append(" - Apply EMCON windows, comms hardening; check for GPS/signal spoofing.") | |
| if merged_scores.get("Deter", 0) >= 2: | |
| lines.append(" - Use visible patrols to shape ENY calculus and protect key nodes.") | |
| if not any(v >= 2 for v in merged_scores.values()): | |
| lines.append(" - No dominant 5D signals; maintain ISR and conduct deception hunt measurements.") | |
| return "\n".join(lines) | |
| # ------------------------- | |
| # Synthesize BULL COAs (Indian Army style) with dynamic triggers | |
| # ------------------------- | |
| def synthesize_bull_coas(merged_scores): | |
| total = sum(merged_scores.values()) if sum(merged_scores.values()) > 0 else 1 | |
| # dynamic thresholds scaled to current intensity | |
| t1 = max(2, round(total / 4)) # for Detect escalation | |
| t2 = max(2, round(total / 3)) # for Deliver+Destroy escalation | |
| t3 = max(1, round(total / 5)) # for Destroy launch | |
| lines = [] | |
| lines.append("COURSES OF ACTION (BULL — INDIAN ARMY STYLE, DYNAMIC TRIGGERS)") | |
| lines.append("\nCOA 1 — DEFENSIVE FORCE PROTECTION (Hold & Harden)") | |
| lines.append(" Mission: Maintain force protection of convoys and bases in AOR.") | |
| lines.append(" Main Effort: ISR & route security.") | |
| lines.append(" Tasks (5D):") | |
| lines.append(" - Detect: Persistent UAS & patrols; assign PIRs to suspected axes.") | |
| lines.append(" - Deny: EMCON windows, comm encryption, route randomization.") | |
| lines.append(" - Deter: Visible mounted patrols & show-of-force.") | |
| lines.append(" - Deliver: Civil protection patrols to reduce collab.") | |
| lines.append(" - Destroy: Hold kinetic response in reserve; interdiction only on confirmed targets.") | |
| lines.append(f" Trigger (to COA 2): Detect intensity ≥ {t1} within 24 hrs OR ≥ {t1} independent detections.") | |
| lines.append("\nCOA 2 — ACTIVE INTERDICTION (Seize Initiative)") | |
| lines.append(" Mission: Disrupt ENY buildup and remove immediate threat nodes.") | |
| lines.append(" Main Effort: QRF, target development, precision interdiction.") | |
| lines.append(" Tasks (5D):") | |
| lines.append(" - Detect: Rapid target confirmation (HUMINT + IMINT).") | |
| lines.append(" - Deny: Seal suspected areas, cut supply lines.") | |
| lines.append(" - Deter: Short, sharp strikes & patrols to unsettle ENY.") | |
| lines.append(" - Deliver: Isolate and protect local assets willing to cooperate.") | |
| lines.append(" - Destroy: Limited precision strikes on IED/weapon caches (ROE compliant).") | |
| lines.append(f" Trigger (to COA 3): Deliver+Destroy combined intensity ≥ {t2} OR confirmed munitions caches.") | |
| lines.append("\nCOA 3 — OFFENSIVE ACTION (Elimination)") | |
| lines.append(" Mission: Destroy ENY operational capability in AOR.") | |
| lines.append(" Main Effort: Offensive operations with full support.") | |
| lines.append(" Tasks (5D):") | |
| lines.append(" - Detect: Full target confirmation (multi-source).") | |
| lines.append(" - Deny: Isolate battle space, interdiction of reinforcement routes.") | |
| lines.append(" - Deter: Suppress ENY freedom of movement via combined arms.") | |
| lines.append(" - Deliver: Post-strike stabilization of population.") | |
| lines.append(" - Destroy: Decisive strikes on leadership/munitions nodes.") | |
| lines.append(f" Trigger: Destroy intensity ≥ {t3} AND at least 2 independent target confirmations.") | |
| lines.append("\nRISKS & MITIGATION:") | |
| lines.append("- Risk: Collateral damage and escalation. Mitigation: Strict ROE and phased escalation.") | |
| lines.append("- Risk: ENY deception. Mitigation: Multi-source confirmation and deception-hunt probes before kinetic action.") | |
| return "\n".join(lines) | |
| # ------------------------- | |
| # Commander note explaining scores and radar | |
| # ------------------------- | |
| def generate_commander_note(merged_scores): | |
| note = [] | |
| note.append("COMMANDER'S NOTE — HOW TO INTERPRET SCORES & RADAR") | |
| note.append("- Scores are intensity indicators (0-5) for ENY activity across 5D functions; higher = more evidence/weight.") | |
| note.append("- Radar spikes show where ENY emphasis lies. A single spike should trigger targeted ISR to verify.") | |
| note.append("- Do NOT treat scores as final truth; they are decision aids. Always insist on multi-source confirmation for kinetic moves.") | |
| note.append("- Use the dynamic COA triggers in the BULL section to move between COAs; triggers scale with overall activity.") | |
| note.append("- If DETECT is high but DESTROY is low => suspect recon/decoy. If DESTROY is high => prepare protection & interdiction.") | |
| return "\n".join(note) | |
| # ------------------------- | |
| # SITREP loader: search repo root and knowledge_base for .json SITREPs | |
| # ------------------------- | |
| def load_sitreps_from_repo(): | |
| sitreps = {} | |
| roots = [Path("."), Path("knowledge_base")] | |
| for r in roots: | |
| if r.exists() and r.is_dir(): | |
| for p in sorted(r.glob("*.json")): | |
| try: | |
| text = p.read_text(encoding="utf-8") | |
| _ = json.loads(text) | |
| sitreps[str(p)] = text | |
| except Exception: | |
| try: | |
| text = p.read_text(encoding="utf-8", errors="ignore") | |
| sitreps[str(p)] = text | |
| except Exception: | |
| continue | |
| return sitreps | |
| # Load at startup | |
| SITREP_FILES = load_sitreps_from_repo() | |
| # ------------------------- | |
| # MAIN analyze pipeline (single entry) | |
| # ------------------------- | |
| def analyze_enemy_full(observation_text): | |
| if not observation_text or not observation_text.strip(): | |
| observation_text = "NO OBSERVATION PROVIDED" | |
| # 1) Keyword fuzzy scoring & evidence | |
| keyword_scores, evidence = fuzzy_score_with_evidence(observation_text, KEYWORDS) | |
| # 2) Try LLM self-score (optional); do not fail if not available | |
| llm_scores, llm_err = None, None | |
| client_info = get_openai_client() | |
| if client_info and client_info[0] is not None: | |
| try: | |
| llm_scores, llm_err = llm_self_score(observation_text) | |
| except Exception as e: | |
| llm_err = str(e) | |
| logger.warning("LLM self-score failed: %s", llm_err) | |
| else: | |
| llm_err = "No OpenAI client configured for self-score" | |
| # 3) Merge | |
| merged = merge_and_normalize_scores(keyword_scores, llm_scores, kw_weight=0.6, llm_weight=0.4, scale_to=5) | |
| # 4) Cross diagnostics | |
| cross_flags = cross_diagnostics(evidence, merged) | |
| # 5) Build observation json for LLM full report | |
| observation_json = { | |
| "observation_id": f"OBS-{datetime.datetime.utcnow().strftime('%Y%m%d-%H%M%S')}", | |
| "timestamp_utc": datetime.datetime.utcnow().isoformat(), | |
| "raw_text": observation_text, | |
| "keyword_scores_raw": keyword_scores, | |
| "llm_scores_raw": llm_scores if llm_scores else {}, | |
| "merged_5d_scores": merged, | |
| "evidence": evidence, | |
| "cross_diagnostics": cross_flags, | |
| "llm_self_score_error": llm_err | |
| } | |
| # 6) Radar chart | |
| fig = make_radar_chart_from_merged(merged) | |
| # 7) Quick COA bullets | |
| quick_coa = [] | |
| if merged.get("Detect", 0) >= 1: | |
| quick_coa.append("Detect: Increase ISR (UAS, SIGINT, HUMINT).") | |
| if merged.get("Deny", 0) >= 1: | |
| quick_coa.append("Deny: Harden comms, EMCON, randomize movement.") | |
| if merged.get("Deter", 0) >= 1: | |
| quick_coa.append("Deter: Visible patrols, QRF posture.") | |
| if merged.get("Deliver", 0) >= 1: | |
| quick_coa.append("Deliver: Civil protection, HUMINT expansion.") | |
| if merged.get("Destroy", 0) >= 1: | |
| quick_coa.append("Destroy: Prepare interdiction and target development.") | |
| if not quick_coa: | |
| quick_coa = ["No clear 5D indicators: prioritize deception-hunting ISR."] | |
| # 8) LLM full two-lens (uses LLM if available; else fallback generator) | |
| llm_full_text = llm_generate_full_report(observation_json) | |
| # 9) Synthesize bulleted SA and BULL COAs and commander note | |
| sa_bulleted = synthesize_bulleted_SA(observation_text, merged, evidence, cross_flags) | |
| bull_coas = synthesize_bull_coas(merged) | |
| cmd_note = generate_commander_note(merged) | |
| # 10) Return outputs | |
| return { | |
| "fig": fig, | |
| "merged_json": merged, | |
| "quick_coa_text": "\n".join(quick_coa), | |
| "llm_full_text": llm_full_text, | |
| "sa_bulleted": sa_bulleted, | |
| "bull_coas": bull_coas, | |
| "commander_note": cmd_note | |
| } | |
| # ------------------------- | |
| # Integrated Warfare Addon (CARVER + BOS + SPTR + Attack on Intent) | |
| # ------------------------- | |
| def carver_analysis_simple(observation_text): | |
| results = [] | |
| lines = observation_text.splitlines() | |
| for line in lines: | |
| if "Target" in line and any(c in line for c in ["C=", "A=", "R=", "V=", "E="]): | |
| try: | |
| parts = line.split() | |
| name = parts[0] + " " + parts[1] if len(parts) > 1 else parts[0] | |
| vals = {} | |
| for p in parts: | |
| if "=" in p and p[0].upper() in ["C","A","R","V","E","G"]: | |
| k = p.split("=")[0] | |
| try: | |
| vals[k] = int(p.split("=")[1]) | |
| except: | |
| vals[k] = 0 | |
| ai = sum(vals.values()) if vals else 0 | |
| results.append(f"{name}: {vals} → AI={ai}") | |
| except Exception: | |
| continue | |
| if not results: | |
| return "No CARVER targets detected. Add lines like 'Target A C=8 A=6 R=5 V=9 E=7 RG=6'." | |
| return "\n".join(results) | |
| def integrated_analysis(observation_text): | |
| scores, evidence = fuzzy_score_with_evidence(observation_text, KEYWORDS) | |
| merged = merge_and_normalize_scores(scores) | |
| fig = make_radar_chart_from_merged(merged) | |
| # LLM narrative if available | |
| client_info = get_openai_client() | |
| narrative = "" | |
| if client_info and client_info[0] is not None: | |
| try: | |
| prompt = f""" | |
| You are an Indian Army staff officer preparing an Integrated MDMP report. | |
| Input SITREP: | |
| {observation_text} | |
| 5D Scores: | |
| {json.dumps(merged, indent=2)} | |
| TASK: | |
| 1. Generate Situational Awareness through 5D + deception lens. | |
| 2. Provide Enemy Intent assessment using CARVER if targets given. | |
| 3. Draft a full Integrated Warfare MDMP plan including: | |
| - 5D Ops Channels | |
| - Attack on Intent–Capability–Action | |
| - SPTR methodology | |
| - BOS-centric wargaming | |
| 4. Output in clear military operational language with headings and bullets. | |
| """ | |
| client, method = client_info | |
| if method == "modern": | |
| resp = client.responses.create(model=os.environ.get("OPENAI_API_MODEL", "gpt-4o-mini"), | |
| input=prompt, max_output_tokens=1000) | |
| for item in getattr(resp, "output", []) or []: | |
| if isinstance(item, dict): | |
| for c in item.get("content", []): | |
| if c.get("type") == "output_text": | |
| narrative += c.get("text", "") | |
| else: | |
| chat = client.ChatCompletion.create(model=os.environ.get("OPENAI_API_MODEL", "gpt-3.5-turbo"), | |
| messages=[{"role":"user","content":prompt}], max_tokens=1000) | |
| narrative = chat.choices[0].message.content | |
| except Exception as e: | |
| narrative = f"Error fetching LLM output: {e}" | |
| else: | |
| narrative = "LLM not configured (no OPENAI_API_KEY set)." | |
| sa_report = f""" | |
| SITUATIONAL AWARENESS (Integrated 5D) | |
| - Enemy Observation (summary): {observation_text[:200]}... | |
| - DETECT Indicators: {merged['Detect']} | |
| - DENY Indicators: {merged['Deny']} | |
| - DETER Indicators: {merged['Deter']} | |
| - DELIVER Indicators: {merged['Deliver']} | |
| - DESTROY Indicators: {merged['Destroy']} | |
| - Assessment: Enemy may be masking intent with deception. | |
| """ | |
| warfare_plan = f""" | |
| INTEGRATED WARFARE BATTLE PLAN | |
| 1. 5D OPS CHANNELS: Detect, Deny, Deter, Deliver, Destroy applied across HUMINT, CI, PsyOps, Strike. | |
| 2. ATTACK ON INTENT–CAPABILITY–ACTION: Degrade leadership, logistics, ISR; impose constraints; degrade/erode capability. | |
| 3. SPTR: Surveillance -> Processing (ACE) -> Targeting (CARVER, folders) -> Response with covert C2 and tactical HUMINT teams. | |
| 4. BOS-CENTRIC WARGAMING: Predict BOSEA, match friendly BOS responses, classify Most Likely / Most Dangerous COAs. | |
| """ | |
| coa_report = f""" | |
| COURSES OF ACTION (Integrated Bull Format) | |
| COA 1 – DEFENSIVE FP: ISR, OPSEC, patrols. Trigger: DENY ≥ 3. | |
| COA 2 – ACTIVE INTERDICTION: Neutralize collaborators, strike caches. Trigger: DELIVER+DESTROY ≥ 4. | |
| COA 3 – OFFENSIVE ACTION: Destroy ENY capability. Trigger: DESTROY ≥ 2 + confirmed staging. | |
| """ | |
| carver_report = carver_analysis_simple(observation_text) | |
| commander_note = """ | |
| COMMANDER’S NOTE (Integrated System) | |
| - Radar spikes show ENY emphasis; but deception may hide real intent. | |
| - Always cross-check high DENY spikes with HUMINT. | |
| - Apply CARVER to rank likely targets; defend most attractive ones. | |
| - Attack Intent, Capability, Action simultaneously. | |
| - Use SPTR for rhythm; BOS wargaming for adaptability. | |
| """ | |
| return narrative, sa_report, warfare_plan, coa_report, carver_report, commander_note, fig | |
| # ------------------------- | |
| # GRADIO UI (single left flush column) | |
| # ------------------------- | |
| custom_css = """ | |
| <style> | |
| /* Remove left padding and make single left-aligned block */ | |
| #leftcol { padding-left: 0px; margin-left: 0px; max-width: 1100px; } | |
| .gradio-container .panel { padding-left: 0px; } | |
| </style> | |
| """ | |
| # Read inline integrated MD if present (also provide link at top) | |
| def read_integrated_md(): | |
| mdp = "knowledge_base/integrated_mdmp_detailed.md" | |
| try: | |
| if os.path.exists(mdp): | |
| with open(mdp, "r", encoding="utf-8") as f: | |
| return f.read() | |
| else: | |
| return None | |
| except Exception as e: | |
| return f"Error reading integrated_mdmp_detailed.md: {e}" | |
| INTEGRATED_MD_TEXT = read_integrated_md() | |
| with gr.Blocks(css=custom_css) as demo: | |
| # Banner (full width) | |
| gr.HTML(f'<img src="{BANNER_URL}" style="width:100%; max-height:200px; object-fit:cover;">') | |
| gr.Markdown(f"# {TITLE}") | |
| gr.Markdown(INTRO) | |
| # Put everything in a single left column block (#leftcol) | |
| with gr.Column(elem_id="leftcol"): | |
| # Top Knowledge Base Links (visible) | |
| with gr.Row(): | |
| kb_links_md = "### Knowledge Base (Quick Links)\n" | |
| for label, path in KNOWLEDGE_BASE.items(): | |
| kb_links_md += f"- **{label}** — `{path}`\n" | |
| gr.Markdown(kb_links_md) | |
| # Inline accordion with full integrated MD (if present) AND a link to file | |
| # Both link and inline are provided as requested | |
| with gr.Accordion("📘 Integrated MDMP — Full Detailed Document (link + inline)", open=False): | |
| link_block = "#### Link to file (if committed to repo):\n" | |
| link_block += f"- [Integrated MDMP document]({KNOWLEDGE_BASE.get('Integrated MDMP (MD)')})\n\n" | |
| gr.Markdown(link_block) | |
| if INTEGRATED_MD_TEXT: | |
| # render inline the entire MD | |
| gr.Markdown(INTEGRATED_MD_TEXT) | |
| else: | |
| gr.Markdown("**integrated_mdmp_detailed.md not found in `knowledge_base/` path.**\n\n" | |
| "Upload `knowledge_base/integrated_mdmp_detailed.md` to see inline content here.") | |
| # Full 5D Questionnaire (collapsed) | |
| with gr.Accordion("Full 5D Questionnaire (open to view)", open=False): | |
| gr.Textbox(value=QUESTIONNAIRE, lines=28, label="Exhaustive Questionnaire (read-only)", interactive=False) | |
| # SITREP loader dropdown + input | |
| sitrep_choices = list(SITREP_FILES.keys()) if SITREP_FILES else [] | |
| sitrep_dropdown = gr.Dropdown(choices=sitrep_choices, label="Load Sample SITREP (from repo / knowledge_base)", interactive=True) | |
| sitrep_load_btn = gr.Button("Load selected SITREP into input") | |
| obs_input = gr.Textbox(label="Observed Enemy Data / SITREP (paste or load)", lines=10, placeholder="Paste raw SITREP text or JSON here...") | |
| # When user selects from dropdown, populate input (pretty-print JSON if JSON) | |
| def load_selected_sitrep(name): | |
| if not name: | |
| return "" | |
| text = SITREP_FILES.get(name, "") | |
| try: | |
| parsed = json.loads(text) | |
| return json.dumps(parsed, indent=2) | |
| except Exception: | |
| return text | |
| sitrep_dropdown.change(load_selected_sitrep, inputs=[sitrep_dropdown], outputs=[obs_input]) | |
| sitrep_load_btn.click(load_selected_sitrep, inputs=[sitrep_dropdown], outputs=[obs_input]) | |
| # Analyze button | |
| analyze_btn = gr.Button("Analyze with 5D MDMP (Two-Lens)") | |
| # Output blocks | |
| llm_out = gr.Textbox(label="LLM Two-Lens Full Report (or Fallback)", lines=22) | |
| sa_out = gr.Textbox(label="Bulleted Situational Awareness (Indian Army)", lines=12) | |
| bull_out = gr.Textbox(label="COA (BULL Format) with Dynamic Triggers", lines=14) | |
| cmdnote_out = gr.Textbox(label="Commander’s Note (How to read scores & radar)", lines=6) | |
| quickcoa_out = gr.Textbox(label="Quick COA Bullets (Reverse-5D) - Immediate", lines=6) | |
| radar_plot = gr.Plot(label="5D Radar (0-5)") | |
| merged_json_out = gr.JSON(label="Merged 5D Scores (0-5)") | |
| # On analyze, run pipeline and output | |
| def on_analyze(obs_text): | |
| result = analyze_enemy_full(obs_text) | |
| fig = result["fig"] | |
| merged = result["merged_json"] | |
| llm_full = result["llm_full_text"] | |
| sa = result["sa_bulleted"] | |
| bull = result["bull_coas"] | |
| cmdnote = result["commander_note"] | |
| quick = result["quick_coa_text"] | |
| return fig, merged, quick, llm_full, sa, bull, cmdnote | |
| analyze_btn.click(on_analyze, | |
| inputs=[obs_input], | |
| outputs=[radar_plot, merged_json_out, quickcoa_out, llm_out, sa_out, bull_out, cmdnote_out]) | |
| # Divider and Integrated Warfare Addon Tab content (inline, flush left) | |
| gr.Markdown("---") | |
| gr.Markdown("## Integrated Warfare MDMP — (SPTR, CARVER, BOS, Attack on Intent)") | |
| with gr.Accordion("Integrated Warfare Analysis (open)", open=False): | |
| obs_input2 = gr.Textbox(label="Paste SITREP / Enemy Observation (Integrated Warfare)", lines=8, placeholder="Enter enemy situation details...") | |
| analyze_btn2 = gr.Button("Run Integrated MDMP (Integrated Warfare Addon)") | |
| narrative_out2 = gr.Textbox(label="LLM Narrative Report (Integrated)", lines=12) | |
| sa_out2 = gr.Textbox(label="Situational Awareness (Integrated)", lines=8) | |
| warfare_out = gr.Textbox(label="Integrated Warfare Plan", lines=12) | |
| coa_out2 = gr.Textbox(label="Courses of Action (Bull Format)", lines=10) | |
| carver_out = gr.Textbox(label="CARVER Target Analysis", lines=6) | |
| commander_note_out2 = gr.Textbox(label="Commander’s Note (Integrated)", lines=8) | |
| radar_out2 = gr.Plot(label="5D Radar Chart (Integrated)") | |
| def on_integrated(obs_text): | |
| narrative, sa_report, warfare_plan, coa_report, carver_report, commander_note, fig = integrated_analysis(obs_text) | |
| return narrative, sa_report, warfare_plan, coa_report, carver_report, commander_note, fig | |
| analyze_btn2.click( | |
| on_integrated, | |
| inputs=[obs_input2], | |
| outputs=[narrative_out2, sa_out2, warfare_out, coa_out2, carver_out, commander_note_out2, radar_out2] | |
| ) | |
| # Knowledge base accordion (read-only links) | |
| with gr.Accordion("Knowledge Base & Sample Files (open)", open=False): | |
| gr.Markdown("The app searches for `.json` in repo root and `knowledge_base/` and lists available sample SITREPs in the dropdown above.") | |
| for label, path in KNOWLEDGE_BASE.items(): | |
| gr.Markdown(f"- **{label}** — `{path}` (place files in `knowledge_base/` or repo root to have them appear in the dropdown).") | |
| # ------------------------- | |
| # Launch | |
| # ------------------------- | |
| if __name__ == "__main__": | |
| demo.launch() | |