SFTRAINING / app.py
Militaryint's picture
Update app.py
76b9bd6 verified
Raw
History Blame Contribute Delete
57.1 kB
# 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)