# 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'|', '\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: ". 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'') 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)