Spaces:
Build error
Build error
| # app.py | |
| # ================================================================ | |
| # THE NEW MILITARY DECISION MAKING PROCESS (MDMP) + UNIT ISR / PARA SF TRAINER | |
| # Author: Keshav Mazumdar (as requested) | |
| # | |
| # This single-file app integrates: | |
| # - 5D questionnaire and scoring engine (keyword + LLM self-score) | |
| # - OPORD generator (UNIT ISR RESPONSE TO ENEMY) as a separate Tab | |
| # - Para SF doctrine DOCX ingestion from knowledge_base/parasf_doctrine.docx | |
| # - Knowledge base links (5d_bos_100_guidelines.md etc.) | |
| # - Chatbot (doctrine/training oriented, non-actionable) | |
| # - PDF generation for reports and OPORDs | |
| # - Radar visualization (5D radar) | |
| # | |
| # NOTE: This file is intentionally long and comprehensive (uncut), as requested. | |
| # ================================================================ | |
| import os | |
| import sys | |
| import json | |
| import re | |
| import datetime | |
| import logging | |
| from pathlib import Path | |
| from collections import defaultdict | |
| from difflib import SequenceMatcher | |
| # Third-party libs | |
| try: | |
| import gradio as gr | |
| except Exception: | |
| raise RuntimeError("Gradio not installed. Run: pip install gradio") | |
| try: | |
| import matplotlib.pyplot as plt | |
| import numpy as np | |
| except Exception: | |
| raise RuntimeError("Matplotlib / numpy not installed. Run: pip install matplotlib numpy") | |
| # PDF generation | |
| try: | |
| from fpdf import FPDF | |
| except Exception: | |
| FPDF = None | |
| # PDF & DOCX reading | |
| try: | |
| import PyPDF2 | |
| except Exception: | |
| PyPDF2 = None | |
| try: | |
| from docx import Document | |
| except Exception: | |
| Document = None | |
| # Data handling | |
| try: | |
| import pandas as pd | |
| except Exception: | |
| pd = None | |
| # Optional: scikit-learn cosine similarity | |
| try: | |
| from sklearn.metrics.pairwise import cosine_similarity | |
| SKLEARN_AVAILABLE = True | |
| except Exception: | |
| SKLEARN_AVAILABLE = False | |
| # Try modern OpenAI client; fallback to classic openai | |
| try: | |
| from openai import OpenAI as OpenAI_Modern | |
| OPENAI_MODERN_AVAILABLE = True | |
| except Exception: | |
| OpenAI_Modern = None | |
| OPENAI_MODERN_AVAILABLE = False | |
| try: | |
| import openai as openai_classic | |
| OPENAI_CLASSIC_AVAILABLE = True | |
| except Exception: | |
| openai_classic = None | |
| OPENAI_CLASSIC_AVAILABLE = False | |
| # Logging | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger("mdmp_app") | |
| # ------------------------- | |
| # Config & Constants | |
| # ------------------------- | |
| TITLE = "THE NEW MILITARY DECISION MAKING PROCESS" | |
| INTRO = ( | |
| "A new military decision making process by Keshav Mazumdar\n" | |
| "(To avert enemy surprise using deception to effect wrong situational awareness)\n\n" | |
| "This MDMP uses the 5D System (Detect, Deny, Deter, Deliver, Destroy)." | |
| ) | |
| # Banner URL (unchanged) | |
| BANNER_URL = "https://huggingface.co/spaces/Militaryint/mdmp/resolve/main/banner.png" | |
| # Knowledge base folder | |
| KB_DIR = Path("knowledge_base") | |
| KB_DIR.mkdir(parents=True, exist_ok=True) | |
| # Knowledge assets filenames (user can replace files in knowledge_base) | |
| 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", | |
| "5D BOS Guidelines (MD)": "knowledge_base/5d_bos_100_guidelines.md", | |
| "PIT ISR Matrix (CSV)": "knowledge_base/pit_isr_matrix.csv", | |
| "Para SF Doctrine (DOCX)": "knowledge_base/parasf_doctrine.docx", | |
| } | |
| # LLM config (environment) | |
| OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "") | |
| OPENAI_API_MODEL = os.environ.get("OPENAI_API_MODEL", "gpt-4o-mini") # or gpt-4o, gpt-4o-mini etc. | |
| EMBEDDING_MODEL = os.environ.get("OPENAI_EMBEDDING_MODEL", "text-embedding-3-small") | |
| # Retrieval parameters | |
| CHUNK_SIZE = 900 | |
| CHUNK_OVERLAP = 150 | |
| TOP_K = 5 | |
| # Safety: keywords & patterns to refuse detailed operational wrongdoing instructions | |
| SENSITIVE_KEYWORDS = [ | |
| "attack", "ambush", "infiltrate", "exfiltrate", "IED", "bomb", "landmine", "weaponize", | |
| "kill", "assassinat", "breach perimeter", "breach perim", "detonate", "how to build", | |
| "how to make", "step by step", "detailed plan", "exact route", "precise coordinates" | |
| ] | |
| SENSITIVE_REGEX = re.compile(r"\b(how to|step by step|detailed plan|exact route|precise coordinates)\b", flags=re.I) | |
| SAFETY_REFUSAL = ( | |
| "I cannot provide step-by-step instructions, detailed attack planning, or operational actions that meaningfully " | |
| "enable violent wrongdoing. I can provide doctrine, training module outlines, exercise design, assessment templates, " | |
| "and legal/ROE guidance. Please ask for any of those." | |
| ) | |
| # ------------------------- | |
| # Exhaustive Questionnaire (UNCUT) | |
| # ------------------------- | |
| 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) | |
| GENERAL COA FIELDS: | |
| - COA ID and short description. | |
| - Hypothesis addressed (Hn). | |
| - Commander’s intent & constraints (ROE, political, resources). | |
| - Priority ISR tasks & timelines to confirm/disconfirm. | |
| - Decision triggers (explicit indicators and thresholds). | |
| - Main effort, supporting efforts, reserve. | |
| - Estimated risk to force and mitigation. | |
| 1) FRIENDLY DETECT (ISR & HUMINT tasks) | |
| - Questions: | |
| - Which sensors (UAS, ground ISR, SIGINT, HUMINT, GEOINT) will best detect the discriminating indicators for Hn? | |
| - What is the ISR sequencing and timeline (persistent, periodic, surge)? | |
| - What specific PIRs (priority intelligence requirements) will you assign? | |
| - How will you avoid revealing ISR coverage (OPSEC of our sensors)? | |
| - How will you test for deception (controlled probes, feints, bait)? | |
| - Deliverables: | |
| - ISR tasking matrix (platform, time-on-target, expected signature, reporting format). | |
| - Expected information yield and acceptable time-to-confirm. | |
| 2) FRIENDLY DENY (Prevent enemy actions & information flow) | |
| - Questions: | |
| - What measures will you take to deny the enemy from detecting or exploiting our vulnerabilities? (route randomization, comm hardening, EMCON) | |
| - What interdiction or area-denial assets do you have (engineers for obstacles, fires, surveillance denial)? | |
| - How will you interdict enemy information passage (capture/turn informers, source ops)? | |
| - What cyber/electronic measures can you apply to blunt enemy ISR? | |
| - Deliverables: | |
| - OPSEC enforcement checklist, comms encryption plan, movement randomization schedule. | |
| 3) FRIENDLY DETER (Posture & influence to dissuade attack) | |
| - Questions: | |
| - What visible steps can create deterrence (mounted patrols, quick reaction teams, artillery presence)? | |
| - What messaging/PSYOP will be used to influence the enemy and locals? | |
| - What hardening is required for convoys/bases (armor, route clearance, signature management)? | |
| - How will deterrence be maintained without escalating? | |
| - Deliverables: | |
| - Show-of-force schedule, civic-messaging lines, escalation ladder. | |
| 4) FRIENDLY DELIVER (Protect & restore local allegiance) | |
| - Questions: | |
| - What population protection measures will reduce enemy influence (convoy escorts, medical aid, economic support)? | |
| - How to remove or neutralize local collaborators (witness protection, incentives, community engagement)? | |
| - Which civil-military projects can reduce grievances exploited by enemy? | |
| - How will HUMINT be expanded ethically to secure local cooperation? | |
| - Deliverables: | |
| - Civil engagement plan, HUMINT collection plan, metrics for local cooperation. | |
| 5) FRIENDLY DESTROY (Targeted neutralization) | |
| - Questions: | |
| - What legal/ROE-compliant direct actions are available? (capture, targeted strike, interdiction) | |
| - Which nodes or capabilities should be targeted first to reduce enemy effectivity? | |
| - What is the desired BDA (battle damage assessment) and follow-up plan for resilient nodes? | |
| - What are the collateral risk estimates and mitigation measures? | |
| - Deliverables: | |
| - Target list with priority, required assets, timelines, and BDA triggers. | |
| 6) SEQUENCING, TRIGGERS & BRANCH PLANS | |
| - Questions: | |
| - What specific Detect indicators will trigger escalation to Deny/Deter/Destroy? | |
| - How long to wait for ISR confirmation before committing to a kinetic COA? | |
| - Which actions create signals that will help detect deception (and how to monitor them)? | |
| - What are abort conditions and fallback COAs? | |
| 7) LOGISTICS, COMMUNICATIONS & COMMAND | |
| - Questions: | |
| - What logistics are required (fuel, medevac, ammunition) and are they available/resilient? | |
| - What is the communications plan and redundant paths? | |
| - Which unit is the decision authority for trigger execution? | |
| --------------------------------------------------------------------- | |
| C) INTERACTIVE Q&A PROMPTS (use this as a guided intake for each observation) | |
| (Officers can answer these in the app; each response becomes an observation record with 5D tags) | |
| 1. Observation timestamp (UTC): | |
| 2. Observer (HUMINT id / sensor type): | |
| 3. Source reliability (0-1): | |
| 4. Location (lat,lon or grid reference): | |
| 5. Observed size and composition (personnel, vehicles, equipment): | |
| 6. Activity observed (movement, encampment, training, convoy, supply drop, other): | |
| 7. What sensors/platforms were observed (UAV, scout, radio, cell phone traffic, checkpoint)? | |
| 8. Evidence of detect activity (describe observers, cameras, listening posts, signals): | |
| 9. Evidence of deny activity (jamming, camouflage, deception, decoys): | |
| 10. Evidence of deter activity (fortifications, show-of-force, checkpoints, propaganda): | |
| 11. Evidence of deliver activity (community engagement by adversary, payments, coerced support): | |
| 12. Evidence of destroy preparation (weapons caches, rehearsals, IED materials, heavy weapons): | |
| 13. Any temporal patterns (time of day, frequency, recent changes): | |
| 14. Raw text notes (free form): | |
| 15. Attachments (image links, SIGINT extract IDs, video times): | |
| 16. Priority question for ISR (what one thing do we need to confirm now?): | |
| 17. Suggested immediate action (monitor, reposition, interdiction, population support, other): | |
| 18. Comments on possible deception (why might this be a lure or false narrative?): | |
| --------------------------------------------------------------------- | |
| D) OUTPUT & USE | |
| - Use the responses above to populate the Situation Hypothesis Table: | |
| Hypothesis ID | Short description | Likelihood (0–1) | Key 5D indicators | Confirming evidence | Disconfirming evidence | Required ISR | Time to test | |
| - For each hypothesis, generate COA families using the Reverse 5D planning questions and produce: | |
| - ISR tasks and timings | |
| - Resource list | |
| - Decision triggers with exact thresholds | |
| - Branch plans and fallback COAs | |
| --------------------------------------------------------------------- | |
| E) DECEPTION-SPECIFIC METRICS (to include in every assessment) | |
| - Source redundancy count (how many independent source types confirm the same fact?) | |
| - Logistic plausibility (are fuel/ammo/supplies consistent with claimed posture?) | |
| - Metadata sanity checks (timestamps, geolocation, comm routing) | |
| - Information asymmetry index (how much of the adversary activity is unknown; percentage) | |
| - Confidence interval (estimate of uncertainty; high/medium/low) | |
| --------------------------------------------------------------------- | |
| F) FINAL REMINDER | |
| - The 5D approach must be iterative: Detect → Test → Refine hypotheses → Plan (Reverse 5D) → Execute → Re-detect | |
| - Always document provenance of evidence and exact triggers for COA branches. | |
| - Keep commander’s intent, ROE, and political constraints explicit in every COA. | |
| """ | |
| # ------------------------- | |
| # Knowledge base loading helpers | |
| # ------------------------- | |
| def read_text_file_safe(path: Path) -> str: | |
| try: | |
| return path.read_text(encoding="utf-8") | |
| except Exception: | |
| try: | |
| return path.read_text(encoding="latin-1") | |
| except Exception: | |
| return "" | |
| def read_docx_safe(path: Path) -> str: | |
| if Document is None: | |
| return "" | |
| try: | |
| doc = Document(path) | |
| paras = [p.text.strip() for p in doc.paragraphs if p.text and p.text.strip()] | |
| return "\n\n".join(paras) | |
| except Exception as e: | |
| logger.warning("read_docx_safe failed: %s", e) | |
| # fallback simple unzip extraction | |
| try: | |
| import zipfile, xml.etree.ElementTree as ET | |
| with zipfile.ZipFile(path) as z: | |
| xml_content = z.read('word/document.xml').decode('utf-8') | |
| text = re.sub(r'</w:p>|</w:tc>', '\n\n', xml_content) | |
| text = re.sub(r'<[^>]+>', '', text) | |
| text = re.sub(r'\n\s*\n+', '\n\n', text) | |
| return text.strip() | |
| except Exception: | |
| return "" | |
| # Load Para SF doctrine if present | |
| PARASF_DOCX_PATH = Path(KNOWLEDGE_BASE.get("Para SF Doctrine (DOCX)", "knowledge_base/parasf_doctrine.docx")) | |
| parasf_text = "" | |
| if PARASF_DOCX_PATH.exists(): | |
| parasf_text = read_docx_safe(PARASF_DOCX_PATH) | |
| logger.info("Loaded Para SF doctrine: chars=%d", len(parasf_text)) | |
| else: | |
| logger.warning("Para SF doctrine not found at %s", PARASF_DOCX_PATH) | |
| # Load 5D BOS guidelines | |
| BOS_GUIDELINES_PATH = Path(KNOWLEDGE_BASE.get("5D BOS Guidelines (MD)", "knowledge_base/5d_bos_100_guidelines.md")) | |
| bos_guidelines_text = read_text_file_safe(BOS_GUIDELINES_PATH) if BOS_GUIDELINES_PATH.exists() else "" | |
| # Load PIT/ISR matrix if present | |
| PIT_ISR_PATH = Path(KNOWLEDGE_BASE.get("PIT ISR Matrix (CSV)", "knowledge_base/pit_isr_matrix.csv")) | |
| if PIT_ISR_PATH.exists() and pd is not None: | |
| try: | |
| pit_isr_df = pd.read_csv(PIT_ISR_PATH) | |
| except Exception as e: | |
| pit_isr_df = pd.DataFrame() | |
| logger.warning("Failed to load PIT ISR matrix: %s", e) | |
| else: | |
| pit_isr_df = pd.DataFrame() | |
| # ------------------------- | |
| # Keyword engine & fuzzy scoring | |
| # ------------------------- | |
| 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", "idm", "attack rehearsals", | |
| "weapons cache", "cache", "ammo dump" | |
| ], | |
| } | |
| def similar(a, b): | |
| return SequenceMatcher(None, a, b).ratio() | |
| def fuzzy_score_with_evidence(text, keywords, token_threshold=0.8): | |
| 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): | |
| if w_low in t or similar(w_low, t) >= token_threshold: | |
| scores[d] += 1 | |
| sent = next((s for s in sentences if t in s.lower()), text) | |
| evidence[d].append({"word": w, "token": t, "index": i, "sentence": sent}) | |
| return scores, evidence | |
| # ------------------------- | |
| # OpenAI client helper | |
| # ------------------------- | |
| def get_openai_client(): | |
| key = OPENAI_API_KEY or os.environ.get("OPENAI_API_KEY", "") | |
| if not key: | |
| return None, "OPENAI_API_KEY missing" | |
| # modern first | |
| if OPENAI_MODERN_AVAILABLE: | |
| try: | |
| client = OpenAI_Modern(api_key=key) | |
| return client, "modern" | |
| except Exception as e: | |
| logger.warning("Modern OpenAI client instantiation failed: %s", e) | |
| if OPENAI_CLASSIC_AVAILABLE: | |
| try: | |
| openai_classic.api_key = key | |
| return openai_classic, "classic" | |
| except Exception as e: | |
| logger.warning("Classic OpenAI client config failed: %s", e) | |
| return None, "No usable OpenAI client available" | |
| # ------------------------- | |
| # LLM self-score for 5D (strict JSON output) | |
| # ------------------------- | |
| LLM_SELF_SCORE_INSTRUCTIONS = """ | |
| You are a military analyst assistant. Read the SITREP text and assign an intensity 0-5 to each of the 5D categories: | |
| Detect, Deny, Deter, Deliver, Destroy. | |
| Return EXACTLY a JSON object with keys detect, deny, deter, deliver, destroy and integer values between 0 and 5. | |
| Include nothing else. | |
| Example: | |
| {"detect":2,"deny":0,"deter":1,"deliver":0,"destroy":0} | |
| SITREP: | |
| --- | |
| {text} | |
| --- | |
| """ | |
| def llm_self_score(sitrep_text): | |
| client_info = get_openai_client() | |
| client, method = client_info if isinstance(client_info, tuple) else (None, "error") | |
| 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=OPENAI_API_MODEL, 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", "") | |
| out_text = out_text.strip() | |
| else: | |
| chat = client.ChatCompletion.create(model=OPENAI_API_MODEL, | |
| messages=[{"role":"user","content":prompt}], max_tokens=200) | |
| out_text = chat.choices[0].message.content.strip() | |
| start = out_text.find("{") | |
| end = out_text.rfind("}") | |
| if start != -1 and end != -1: | |
| json_text = out_text[start:end+1] | |
| try: | |
| parsed = json.loads(json_text) | |
| normalized = { | |
| "Detect": int(parsed.get("detect", parsed.get("Detect", 0))), | |
| "Deny": int(parsed.get("deny", parsed.get("Deny", 0))), | |
| "Deter": int(parsed.get("deter", parsed.get("Deter", 0))), | |
| "Deliver": int(parsed.get("deliver", parsed.get("Deliver", 0))), | |
| "Destroy": int(parsed.get("destroy", parsed.get("Destroy", 0))), | |
| } | |
| return normalized, None | |
| except Exception as e: | |
| return None, f"Failed parsing JSON from LLM: {e}; raw: {out_text}" | |
| else: | |
| return None, f"No JSON found in LLM response: {out_text}" | |
| except Exception as e: | |
| logger.exception("LLM self-score exception") | |
| return None, str(e) | |
| # ------------------------- | |
| # Merge and normalize scores | |
| # ------------------------- | |
| 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 else 1 | |
| if max_kw == 0: | |
| max_kw = 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 | |
| # ------------------------- | |
| # LLM full report prompt & generator (two-lens) | |
| # ------------------------- | |
| 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: | |
| For each COA include: | |
| - COA ID and short description | |
| - Hypothesis addressed | |
| - Commander intent & constraints | |
| - Priority ISR tasks (platform, time-on-target) | |
| - Friendly DETECT tasks | |
| - Friendly DENY tasks | |
| - Friendly DETER tasks | |
| - Friendly DELIVER tasks | |
| - Friendly DESTROY tasks | |
| - Decision triggers with numeric thresholds | |
| - Sequencing, branch/fallback plans, risk estimate & mitigation | |
| - Logistics & communications summary | |
| - BDA metrics | |
| 4) Output order: A) INITIAL SITUATIONAL AWARENESS, Situation Hypotheses Table, COA families, EXECUTIVE SUMMARY | |
| 5) Use bullet lists and explicit headings. If any observation fields are missing, mark them as "MISSING: <field>". | |
| 6) If the keyword engine produced zero scores, still generate full sections and mark inferred items as "INFERRED (low confidence)". | |
| Produce the output as human-readable text suitable for commanders, with clear bullets and headings. | |
| """ | |
| def llm_generate_full_report(observation_json): | |
| client_info = get_openai_client() | |
| client, method = client_info if isinstance(client_info, tuple) else (None, "error") | |
| if client is None: | |
| return "⚠️ OpenAI not configured. Set OPENAI_API_KEY." | |
| prompt = LLM_PROMPT_TEMPLATE.format(observation_json=json.dumps(observation_json, indent=2)) | |
| try: | |
| if method == "modern": | |
| resp = client.responses.create(model=OPENAI_API_MODEL, 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 else str(resp) | |
| else: | |
| chat = client.ChatCompletion.create(model=OPENAI_API_MODEL, | |
| messages=[{"role":"user","content":prompt}], max_tokens=1500) | |
| return chat.choices[0].message.content.strip() | |
| except Exception as e: | |
| logger.exception("LLM generate report failed") | |
| return f"Error calling OpenAI: {e}" | |
| # ------------------------- | |
| # Radar chart helper | |
| # ------------------------- | |
| 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.2, str(values[i]), horizontalalignment='center', verticalalignment='bottom', fontsize=9) | |
| ax.set_title("5D Spectrum (0-5 intensity scale)") | |
| return fig | |
| # ------------------------- | |
| # Cross diagnostics | |
| # ------------------------- | |
| def cross_diagnostics(evidence): | |
| flags = [] | |
| if len(evidence.get("Detect", [])) >= 2 and len(evidence.get("Deny", [])) >= 2: | |
| flags.append("High DETECT and high DENY simultaneously — potential deception; require source redundancy.") | |
| for d, evlist in evidence.items(): | |
| distinct_sentences = len(set([e.get("sentence", "") for e in evlist])) | |
| if distinct_sentences < 2 and len(evlist) > 1: | |
| flags.append(f"{d.upper()} indicators lack sentence-level redundancy.") | |
| return flags | |
| # ------------------------- | |
| # Main analysis pipeline | |
| # ------------------------- | |
| def analyze_enemy(observation_text): | |
| # 1) keyword scoring | |
| keyword_scores, evidence = fuzzy_score_with_evidence(observation_text, KEYWORDS) | |
| # 2) attempt llm self-score | |
| 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" | |
| # 3) merge | |
| merged = merge_and_normalize_scores(keyword_scores, llm_scores, kw_weight=0.6, llm_weight=0.4, scale_to=5) | |
| # 4) observation json | |
| 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 is not None else {}, | |
| "merged_5d_scores": merged, | |
| "evidence": evidence, | |
| "cross_diagnostics": cross_diagnostics(evidence), | |
| "llm_self_score_error": llm_err | |
| } | |
| # 5) radar | |
| fig = make_radar_chart_from_merged(merged) | |
| # 6) quick COA bullets | |
| coa_lines = [] | |
| if merged.get("Detect", 0) >= 1: | |
| coa_lines.append("Detect: Increase ISR with persistent UAS, dedicated SIGINT sweep, HUMINT patrols. Assign PIRs.") | |
| if merged.get("Deny", 0) >= 1: | |
| coa_lines.append("Deny: Harden comms, implement EMCON windows, randomize routes and timings.") | |
| if merged.get("Deter", 0) >= 1: | |
| coa_lines.append("Deter: Visible posture, mounted patrols, QRF readiness.") | |
| if merged.get("Deliver", 0) >= 1: | |
| coa_lines.append("Deliver: Civil-military engagement, protective measures for collaborators, HUMINT expansion.") | |
| if merged.get("Destroy", 0) >= 1: | |
| coa_lines.append("Destroy: Prepare interdiction plans, target development, ROE checks.") | |
| if not coa_lines: | |
| coa_lines = ["No clear strong indicators from automatic scoring. Recommend targeted ISR to remove ambiguity and test deception hypotheses."] | |
| coa_text = "\n".join(coa_lines) | |
| # 7) full llm text | |
| llm_text = llm_generate_full_report(observation_json) | |
| return fig, json.dumps(merged, indent=2), coa_text, llm_text | |
| # ------------------------- | |
| # PDF generation helpers | |
| # ------------------------- | |
| def generate_pdf_report(text, title_prefix="mdmp_report"): | |
| if FPDF is None: | |
| raise RuntimeError("FPDF not installed. pip install fpdf2 or fpdf") | |
| pdf = FPDF() | |
| pdf.add_page() | |
| pdf.set_auto_page_break(auto=True, margin=12) | |
| pdf.set_font("Arial", size=11) | |
| # Title | |
| pdf.set_font("Arial", "B", 12) | |
| pdf.multi_cell(0, 7, title_prefix, align='C') | |
| pdf.ln(4) | |
| pdf.set_font("Arial", size=11) | |
| for line in text.split("\n"): | |
| pdf.multi_cell(0, 6, line) | |
| filename = f"{title_prefix}_{datetime.datetime.utcnow().strftime('%Y%m%d_%H%M%S')}.pdf" | |
| pdf.output(filename) | |
| return filename | |
| # ------------------------- | |
| # OPORD builder (UNIT ISR) | |
| # ------------------------- | |
| def _build_opord_text(observation_json, llm_full_text, merged_scores, coa_quick): | |
| try: | |
| merged_str = json.dumps(merged_scores, indent=2) if isinstance(merged_scores, (dict, list)) else str(merged_scores) | |
| except Exception: | |
| merged_str = str(merged_scores) | |
| header = ( | |
| "UNIT ISR RESPONSE REPORT\n" | |
| f"Observation ID: {observation_json.get('observation_id', 'MISSING')}\n" | |
| f"Generated UTC: {datetime.datetime.utcnow().isoformat()}\n" | |
| "Reference: Observation example: Three armed suspects sighted at Location XY (sample)\n" | |
| "---\n\n" | |
| ) | |
| body = header | |
| body += "## 1. SITUATION\n\n" | |
| body += "**a. Enemy Activity (extracted / consolidated):**\n\n" | |
| detect = merged_scores.get("Detect", "MISSING") if isinstance(merged_scores, dict) else "MISSING" | |
| deny = merged_scores.get("Deny", "MISSING") if isinstance(merged_scores, dict) else "MISSING" | |
| deter = merged_scores.get("Deter", "MISSING") if isinstance(merged_scores, dict) else "MISSING" | |
| deliver = merged_scores.get("Deliver", "MISSING") if isinstance(merged_scores, dict) else "MISSING" | |
| destroy = merged_scores.get("Destroy", "MISSING") if isinstance(merged_scores, dict) else "MISSING" | |
| body += f"- Detect (auto score): {detect}\n" | |
| body += f"- Deny (auto score): {deny}\n" | |
| body += f"- Deter (auto score): {deter}\n" | |
| body += f"- Deliver (auto score): {deliver}\n" | |
| body += f"- Destroy (auto score): {destroy}\n\n" | |
| body += "Detailed LLM/Analyst synthesis (two-lens):\n\n" | |
| if llm_full_text: | |
| body += llm_full_text + "\n\n" | |
| else: | |
| body += "MISSING: LLM detailed analysis not available.\n\n" | |
| body += "---\n## 2. MISSION (one-line)\n\n" | |
| body += "**By [Unit], at [time], to deny the enemy the ability to ambush/strike traffic on Route [ref] near Location XY, defeat hostile elements observed at XY, and restore secure movement on the route while preserving civilian life and complying with ROE.**\n\n" | |
| body += "---\n## 3. EXECUTION\n\n" | |
| body += "**Commander’s Intent:** Prevent an ambush; protect friendly convoys and civilians; identify and either capture or neutralize hostile elements. Use intelligence-led operations.\n\n" | |
| body += "**Concept of Operations (Phases):**\n" | |
| body += "- Phase 0 — Detect & Confirm (Immediate, T+0–T+60 minutes)\n" | |
| body += "- Phase 1 — Deny & Harden (T+30–T+90)\n" | |
| body += "- Phase 2 — Fix & Isolate (Trigger-based)\n" | |
| body += "- Phase 3 — Assault / Interdict / Capture (Decision Authority: Unit CO)\n" | |
| body += "- Phase 4 — Consolidation & BDA\n\n" | |
| body += "Tasks to subordinate units:\n- TOC (Forward): Maintain C2, manage ISR tasking.\n- RS Bravo: Covert observation, immediate reporting.\n- OTA: Stage, prepare blocking positions.\n- FS & FG: Hold fire until positive ID.\n\n" | |
| body += "---\nDecision Triggers (summary):\n" | |
| body += "- T1 (Investigate): corroborated second source of 3+ armed at XY within 1 hour -> Phase 1.\n" | |
| body += "- T2 (Hold & Harden): IED components or distance-to-road ≤ 50 m -> Harden route.\n" | |
| body += "- T3 (Engage): Positive ID and hostile intent to ambush or direct hostile fire -> Phase 3.\n" | |
| body += "- Abort: Civilian presence or conflicting HUMINT -> withdraw to surveillance.\n\n" | |
| body += "---\n## 4. SUSTAINMENT (Logistics & Medical)\n" | |
| body += "- Logistics: Ammo, QRF refuel, engineers on call.\n- Medical: CASEVAC plan, CCP at forward TOC.\n\n" | |
| body += "---\n## 5. COMMAND & SIGNAL\n" | |
| body += "- Command: Unit CO decision authority.\n- Communications: Primary encrypted V/UHF net; secondary data links.\n\n" | |
| body += "---\n## ANNEX A — ISR TASKING MATRIX\n" | |
| body += "Priority Intelligence Requirements (PIRs):\n" | |
| body += "1) Weapon types and quantities (HIGH) — UAS + RS Bravo\n" | |
| body += "2) Presence of additional personnel/vehicles within 1 km (HIGH)\n" | |
| body += "3) Source redundancy/local support indicators (MED)\n\n" | |
| body += "---\n## ANNEX B — RECONNAISSANCE & SURVEILLANCE (RS) PLAN\n" | |
| body += "RS Bravo disposition: Deployed 2 km from target, 4 subteams. ROE: no engagement unless fired upon.\n\n" | |
| body += "---\n## ANNEX C — FORCE PROTECTION (Shortcomings & Corrections)\n" | |
| body += "- Ops/Int integration gaps flagged; recommend separate Intel Officer, CI processes, randomness in movement, source registry.\n\n" | |
| body += "---\n## ANNEX D — COA FAMILIES (Reverse-5D)\n" | |
| body += "- COA 1: ISR-Led Fix & Capture (Main effort detect & capture).\n- COA 2: Cordon & Wait (Deny & Attrit)\n\n" | |
| body += "---\n## ANNEX E — COUNTERINTELLIGENCE & SOURCE VETTING\n" | |
| body += "- Vet initial HUMINT via registry; require independent corroboration before kinetic action.\n\n" | |
| body += "---\nEXECUTIVE SUMMARY:\n" | |
| body += "Three armed suspects reported near Route XY; immediate ISR confirmation required. Prepared COAs: ISR-led capture or cordon & wait.\n\n" | |
| body += "END OPORD\n" | |
| body += "\n---\nAUTOMATIC METRICS (merged 5D scores):\n" + merged_str + "\n\n" | |
| body += "QUICK COA BULLETS (auto):\n" + (coa_quick or "No COA suggested automatically.") + "\n" | |
| return body | |
| # ------------------------- | |
| # UNIT ISR Tab handlers | |
| # ------------------------- | |
| def _save_pdf_from_text(text, fallback_prefix="unit_isr_opord"): | |
| try: | |
| if "generate_pdf_report" in globals() and callable(generate_pdf_report): | |
| fname = generate_pdf_report(text) | |
| return fname | |
| except Exception: | |
| pass | |
| try: | |
| if FPDF is None: | |
| return None | |
| pdf = FPDF() | |
| pdf.add_page() | |
| pdf.set_auto_page_break(auto=True, margin=12) | |
| pdf.set_font("Arial", size=11) | |
| for line in text.split("\n"): | |
| try: | |
| pdf.multi_cell(0, 6, line) | |
| except Exception: | |
| pdf.multi_cell(0, 6, line.encode("utf-8", errors="replace").decode("utf-8", errors="replace")) | |
| fname = f"{fallback_prefix}_{datetime.datetime.utcnow().strftime('%Y%m%d_%H%M%S')}.pdf" | |
| pdf.output(fname) | |
| return fname | |
| except Exception as e: | |
| logger.exception("Fallback PDF writer failed: %s", e) | |
| return None | |
| # ------------------------- | |
| # Knowledge-based Chatbot helpers (Docx ingestion + retrieval) | |
| # ------------------------- | |
| def extract_text_from_pdf(path: Path) -> str: | |
| if PyPDF2 is None: | |
| return "" | |
| text_blocks = [] | |
| try: | |
| with open(path, "rb") as fh: | |
| reader = PyPDF2.PdfReader(fh) | |
| for p in range(len(reader.pages)): | |
| page = reader.pages[p] | |
| try: | |
| txt = page.extract_text() or "" | |
| except Exception: | |
| txt = "" | |
| text_blocks.append(txt) | |
| except Exception as e: | |
| logger.warning("PDF extraction failed for %s: %s", path, e) | |
| return "\n\n".join(text_blocks) | |
| def chunk_text(text: str, size: int = CHUNK_SIZE, overlap: int = CHUNK_OVERLAP) -> list: | |
| if not text: | |
| return [] | |
| chunks = [] | |
| start = 0 | |
| length = len(text) | |
| while start < length: | |
| end = min(start + size, length) | |
| chunk = text[start:end] | |
| chunks.append(chunk.strip()) | |
| if end == length: | |
| break | |
| start = end - overlap | |
| return chunks | |
| # Minimal fallback vector (deterministic) | |
| def simple_text_vector(text: str, dims: int = 384) -> np.ndarray: | |
| vec = np.zeros(dims, dtype=float) | |
| words = re.findall(r"\w+", text.lower()) | |
| for i, w in enumerate(words[:dims]): | |
| h = abs(hash(w)) % dims | |
| vec[h] += 1.0 | |
| norm = np.linalg.norm(vec) | |
| return vec / (norm + 1e-8) | |
| # Build KB from files in knowledge_base (pdf/docx/md) | |
| KB = {"documents": [], "embeddings": None} | |
| def build_kb_from_files(manual_files): | |
| kb = {"documents": [], "embeddings": None} | |
| all_texts = [] | |
| for fname in manual_files: | |
| fpath = Path(fname) | |
| if not fpath.exists(): | |
| continue | |
| ext = fpath.suffix.lower() | |
| if ext in [".pdf"]: | |
| text = extract_text_from_pdf(fpath) | |
| elif ext in [".docx"]: | |
| text = read_docx_safe(fpath) | |
| else: | |
| text = read_text_file_safe(fpath) | |
| chunks = chunk_text(text) | |
| for i, c in enumerate(chunks): | |
| doc_id = f"{fpath.name}::chunk_{i+1}" | |
| kb["documents"].append({ | |
| "id": doc_id, | |
| "source": fpath.name, | |
| "chunk_index": i+1, | |
| "text": c | |
| }) | |
| all_texts.append(c) | |
| # compute embeddings (OpenAI) else fallback | |
| embeddings = None | |
| if OPENAI_API_KEY and (OPENAI_MODERN_AVAILABLE or OPENAI_CLASSIC_AVAILABLE): | |
| embeddings = compute_embeddings_openai(all_texts) | |
| if embeddings is None: | |
| embeddings = np.array([simple_text_vector(t) for t in all_texts]) if all_texts else np.zeros((0,384)) | |
| kb["embeddings"] = embeddings | |
| return kb | |
| def compute_embeddings_openai(texts): | |
| if not texts: | |
| return None | |
| try: | |
| if OPENAI_MODERN_AVAILABLE: | |
| client = OpenAI_Modern(api_key=OPENAI_API_KEY) | |
| embs = [] | |
| batch_size = 16 | |
| for i in range(0, len(texts), batch_size): | |
| batch = texts[i:i+batch_size] | |
| resp = client.embeddings.create(model=EMBEDDING_MODEL, input=batch) | |
| for item in resp.data: | |
| embs.append(np.array(item.embedding, dtype=float)) | |
| return np.vstack(embs) | |
| elif OPENAI_CLASSIC_AVAILABLE: | |
| openai_classic.api_key = OPENAI_API_KEY | |
| embs = [] | |
| batch_size = 16 | |
| for i in range(0, len(texts), batch_size): | |
| batch = texts[i:i+batch_size] | |
| resp = openai_classic.Embedding.create(input=batch, engine=EMBEDDING_MODEL) | |
| for item in resp["data"]: | |
| embs.append(np.array(item["embedding"], dtype=float)) | |
| return np.vstack(embs) | |
| except Exception as e: | |
| logger.warning("OpenAI embeddings failed: %s", e) | |
| return None | |
| def retrieve_relevant(kb, query, top_k=TOP_K): | |
| if not kb or not kb.get("documents"): | |
| return [] | |
| # embed query | |
| if OPENAI_API_KEY and (OPENAI_MODERN_AVAILABLE or OPENAI_CLASSIC_AVAILABLE): | |
| q_emb = compute_embeddings_openai([query]) | |
| if q_emb is None: | |
| q_emb = simple_text_vector(query).reshape(1, -1) | |
| else: | |
| q_emb = simple_text_vector(query).reshape(1, -1) | |
| embs = kb["embeddings"] | |
| if embs is None or embs.shape[0] == 0: | |
| return [] | |
| try: | |
| if SKLEARN_AVAILABLE: | |
| sims = cosine_similarity(q_emb, embs)[0] | |
| else: | |
| sims = np.dot(embs, q_emb[0]) | |
| except Exception: | |
| sims = np.dot(embs, q_emb[0]) | |
| top_idx = np.argsort(-sims)[:top_k] | |
| results = [] | |
| for idx in top_idx: | |
| doc = kb["documents"][idx].copy() | |
| doc["score"] = float(sims[idx]) | |
| results.append(doc) | |
| return results | |
| # Initialize KB with files we know | |
| MANUAL_FILES = [] | |
| for v in KNOWLEDGE_BASE.values(): | |
| p = Path(v) | |
| if p.exists() and p.is_file(): | |
| MANUAL_FILES.append(str(p)) | |
| # also add the Para SF doctrine path explicitly if exists | |
| if PARASF_DOCX_PATH.exists(): | |
| if str(PARASF_DOCX_PATH) not in MANUAL_FILES: | |
| MANUAL_FILES.append(str(PARASF_DOCX_PATH)) | |
| KB = build_kb_from_files(MANUAL_FILES) | |
| logger.info("KB loaded: %d documents", len(KB.get("documents", []))) | |
| # ------------------------- | |
| # Chatbot safety & synthesis | |
| # ------------------------- | |
| def is_sensitive_query(user_text: str) -> bool: | |
| low = user_text.lower() | |
| if SENSITIVE_REGEX.search(user_text): | |
| return True | |
| for kw in SENSITIVE_KEYWORDS: | |
| if kw.lower() in low: | |
| return True | |
| return False | |
| def synthesize_answer(retrieved_chunks, user_question, kb_context_limit=3): | |
| if is_sensitive_query(user_question): | |
| return SAFETY_REFUSAL | |
| context_parts = [] | |
| for c in retrieved_chunks[:kb_context_limit]: | |
| citation = f"[{c['source']} - chunk {c['chunk_index']}]" | |
| context_parts.append(f"{citation}\n{c['text']}\n") | |
| prompt_context = "\n\n".join(context_parts) | |
| prompt = ( | |
| "You are a doctrine/training assistant. Use the following NON-ACTIONABLE source excerpts to answer the user's question.\n\n" | |
| "SOURCE EXCERPTS:\n" | |
| f"{prompt_context}\n\n" | |
| "USER QUESTION:\n" | |
| f"{user_question}\n\n" | |
| "INSTRUCTIONS:\n" | |
| "- Provide clear, doctrinal, training-focused answer. Use simple military English.\n" | |
| "- Do NOT provide step-by-step operational instructions, weapon instructions, or any details that enable violent wrongdoing.\n" | |
| "- If user's request is sensitive, refuse with a short safety message and offer safe alternatives (training, doctrine, simulation, assessment templates).\n" | |
| "- Cite the source excerpts used (by their citation bracket) in-line.\n" | |
| ) | |
| client_info = get_openai_client() | |
| client, method = client_info if isinstance(client_info, tuple) else (None, "error") | |
| if client is None: | |
| # fallback conservative synthesis | |
| lines = ["SYNTHESIS (no LLM available):\n"] | |
| for c in retrieved_chunks[:kb_context_limit]: | |
| lines.append(f"Source: {c['source']} (chunk {c['chunk_index']})\n") | |
| safe_sentences = [] | |
| for sent in re.split(r'(?<=[.!?])\s+', c['text']): | |
| if re.search(r"\b(?:kill|attack|ambush|infiltrate|exfiltrate|detonate|IED|bomb|weapon)\b", sent, flags=re.I): | |
| continue | |
| safe_sentences.append(sent.strip()) | |
| lines.append(" ".join(safe_sentences[:3])) | |
| lines.append("") | |
| lines.append("\nIf you need more doctrinal detail (non-actionable) ask for: training module outlines, exercise designs, assessment metrics, or ROE guidance.") | |
| return "\n".join(lines) | |
| try: | |
| if method == "modern": | |
| resp = client.responses.create(model=OPENAI_API_MODEL, input=prompt, max_output_tokens=800) | |
| 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", "") | |
| answer = out_text.strip() if out_text else "No LLM output." | |
| return answer | |
| else: | |
| openai_classic.api_key = OPENAI_API_KEY | |
| chat_resp = openai_classic.ChatCompletion.create( | |
| model=OPENAI_API_MODEL, | |
| messages=[{"role":"system","content":"You are a helpful, safety-conscious military doctrine assistant."}, | |
| {"role":"user","content":prompt}], | |
| max_tokens=800, | |
| temperature=0.2 | |
| ) | |
| return chat_resp.choices[0].message.content.strip() | |
| except Exception as e: | |
| logger.warning("LLM synth failed: %s", e) | |
| # fallback to non-LLM answer | |
| return synthesize_answer([], user_question, kb_context_limit=kb_context_limit) | |
| # ------------------------- | |
| # UI: Gradio Blocks with multiple Tabs | |
| # ------------------------- | |
| with gr.Blocks(title="MDMP + UNIT ISR + PARA SF Trainer") as demo: | |
| # Header | |
| gr.HTML(f'<img src="{BANNER_URL}" width="100%">') | |
| gr.Markdown(f"# {TITLE}\n\n{INTRO}") | |
| # Accordion with full questionnaire | |
| with gr.Accordion("Full 5D Questionnaire (open to view)", open=False): | |
| gr.Textbox(value=QUESTIONNAIRE, lines=40, label="Exhaustive Questionnaire", interactive=False) | |
| # Interactive Analysis Tab (core MDMP) | |
| with gr.Tab("Interactive Analysis"): | |
| obs = gr.Textbox(lines=8, placeholder="Paste observed enemy data or SITREP here...", label="Observed Enemy Data") | |
| btn = gr.Button("Analyze with 5D + Full Report") | |
| radar = gr.Plot(label="Radar Chart") | |
| scores = gr.JSON(label="Merged Score Details (0-5)") | |
| coa_out = gr.Textbox(label="Suggested COA (Reverse 5D) - Quick Bullets", interactive=False) | |
| llm_out = gr.Textbox(label="ChatGPT Military Report (Two-Lens)", interactive=False, lines=25) | |
| download_btn = gr.Button("Download Report as PDF") | |
| file_out = gr.File() | |
| def run_analysis(obs_text): | |
| fig, sc, coa_text, llm_text = analyze_enemy(obs_text) | |
| return fig, sc, coa_text, llm_text | |
| btn.click(run_analysis, inputs=obs, outputs=[radar, scores, coa_out, llm_out]) | |
| def save_report(llm_text): | |
| fname = generate_pdf_report(llm_text) | |
| return fname | |
| download_btn.click(save_report, inputs=llm_out, outputs=file_out) | |
| # Knowledge Base Tab | |
| with gr.Tab("Knowledge Base"): | |
| gr.Markdown("### Reference Documents") | |
| for label, path in KNOWLEDGE_BASE.items(): | |
| gr.Markdown(f"- **{label}** — `{path}`") | |
| # UNIT ISR RESPONSE TO ENEMY — Tab inserted flush-left as separate block | |
| with gr.Tab("UNIT ISR RESPONSE TO ENEMY"): | |
| gr.Markdown( | |
| "## UNIT ISR RESPONSE TO ENEMY — OPORD Generator\n\n" | |
| "Paste a SITREP (free text) and click **Generate OPORD**. This tab re-uses the app pipeline\n" | |
| "(keyword engine + optional OpenAI calls) to produce a commander-ready OPORD and a PDF.\n\n" | |
| "The OPORD template is the unit-level Operational Order format (SITUATION / MISSION / EXECUTION / SUSTAINMENT / C2 & SIGNAL / ANNEXES).\n" | |
| ) | |
| sitrep_in = gr.Textbox(lines=10, label="Paste SITREP (raw text)") | |
| gen_btn = gr.Button("Generate OPORD") | |
| opord_text = gr.Textbox(lines=40, label="UNIT ISR OPORD (Text Output)") | |
| opord_pdf = gr.File(label="Download OPORD PDF (timestamped)") | |
| def _on_generate(sitrep_text): | |
| if not sitrep_text or not str(sitrep_text).strip(): | |
| return "ERROR: No SITREP provided.", None | |
| try: | |
| fig, merged_json_str, coa_text, llm_text = analyze_enemy(sitrep_text) | |
| except Exception as e: | |
| return f"ERROR: analyze_enemy failed: {e}", None | |
| try: | |
| merged_scores = json.loads(merged_json_str) if isinstance(merged_json_str, str) else merged_json_str | |
| except Exception: | |
| merged_scores = merged_json_str | |
| try: | |
| observation_json = { | |
| "observation_id": merged_scores.get("observation_id", f"OBS-{datetime.datetime.utcnow().strftime('%Y%m%d-%H%M%S')}") if isinstance(merged_scores, dict) else f"OBS-{datetime.datetime.utcnow().strftime('%Y%m%d-%H%M%S')}", | |
| "timestamp_utc": datetime.datetime.utcnow().isoformat(), | |
| "raw_text": sitrep_text, | |
| "merged_5d_scores": merged_scores | |
| } | |
| llm_full = llm_generate_full_report(observation_json) | |
| except Exception: | |
| llm_full = llm_text or "" | |
| try: | |
| opord_body = _build_opord_text(observation_json, llm_full, merged_scores, coa_text) | |
| except Exception as e: | |
| return f"ERROR building OPORD text: {e}", None | |
| try: | |
| pdf_fname = _save_pdf_from_text(opord_body) | |
| except Exception: | |
| pdf_fname = None | |
| return opord_body, pdf_fname | |
| gen_btn.click(fn=_on_generate, inputs=[sitrep_in], outputs=[opord_text, opord_pdf]) | |
| # Para SF Training Chatbot Tab (doctrine/training focus) | |
| with gr.Tab("Para SF Trainer Chatbot"): | |
| gr.Markdown("Ask doctrinal, training, or assessment questions (non-actionable). The assistant will cite the Para SF doctrine and 5D BOS guidelines where relevant.") | |
| chat_in = gr.Textbox(lines=2, placeholder="e.g. 'Design a 36-hour stress test module with evaluation criteria'", label="Your question") | |
| chat_btn = gr.Button("Ask Chatbot") | |
| chat_out = gr.Textbox(lines=16, label="Assistant reply") | |
| history_state = gr.State(json.dumps([])) | |
| kb_status = gr.Textbox(label="KB Status", interactive=False) | |
| def on_chat(q, hist_json): | |
| if not q or not q.strip(): | |
| return "Please enter a question.", hist_json | |
| # check sensitivity | |
| if is_sensitive_query(q): | |
| ans = SAFETY_REFUSAL | |
| hist = [] | |
| try: | |
| hist = json.loads(hist_json) if hist_json else [] | |
| except Exception: | |
| hist = [] | |
| hist.append({"user": q, "assistant": ans}) | |
| return ans, json.dumps(hist) | |
| # retrieve | |
| retrieved = retrieve_relevant(KB, q, top_k=TOP_K) | |
| ans = synthesize_answer(retrieved, q, kb_context_limit=3) | |
| hist = [] | |
| try: | |
| hist = json.loads(hist_json) if hist_json else [] | |
| except Exception: | |
| hist = [] | |
| hist.append({"user": q, "assistant": ans}) | |
| return ans, json.dumps(hist) | |
| chat_btn.click(on_chat, inputs=[chat_in, history_state], outputs=[chat_out, history_state]) | |
| def show_kb_status(): | |
| count = len(KB["documents"]) if KB and KB.get("documents") else 0 | |
| return f"KB documents: {count}. Files: {MANUAL_FILES}" | |
| kb_status.value = show_kb_status() | |
| # Admin Tab: reload KB, see status, small utilities | |
| with gr.Tab("Admin / Utilities"): | |
| gr.Markdown("Reload KB, view files, and manage settings.") | |
| reload_btn = gr.Button("Reload Knowledge Base") | |
| kb_status2 = gr.Textbox(label="KB Status (reloadable)", interactive=False) | |
| def reload_kb(): | |
| global KB | |
| KB = build_kb_from_files(MANUAL_FILES) | |
| return show_kb_status() | |
| reload_btn.click(reload_kb, inputs=None, outputs=kb_status2) | |
| kb_status2.value = show_kb_status() | |
| # Footer note | |
| gr.Markdown("---\n**Safety:** This application will not provide operational step-by-step instructions for violent wrongdoing. It is designed for doctrine, training, and intelligence planning support only.") | |
| # ------------------------- | |
| # Launch | |
| # ------------------------- | |
| if __name__ == "__main__": | |
| # If running in an environment (like Hugging Face Spaces) that requires a specific host/port, | |
| # you can set them via env variables or change these defaults. | |
| server_name = os.environ.get("GRADIO_SERVER_NAME", "0.0.0.0") | |
| server_port = int(os.environ.get("GRADIO_SERVER_PORT", "7860")) | |
| demo.launch(server_name=server_name, server_port=server_port, share=False) | |