Militaryint commited on
Commit
a712f50
·
verified ·
1 Parent(s): 9659751

Update APPC.py

Browse files
Files changed (1) hide show
  1. APPC.py +571 -198
APPC.py CHANGED
@@ -1,226 +1,599 @@
1
- # app.py — Full, unhidden, sanitized UI with banner, OpenAI advisory toggle, and flattened Gradio inputs
 
 
 
 
 
 
 
2
  import os
 
 
 
 
3
  import gradio as gr
4
 
5
- # --- Optional OpenAI integration ---
 
6
  try:
7
- import openai
8
- OPENAI_AVAILABLE = True
9
- OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") or ""
10
- if OPENAI_API_KEY:
11
- openai.api_key = OPENAI_API_KEY
12
- else:
13
- OPENAI_AVAILABLE = False
14
  except Exception:
15
- OPENAI_AVAILABLE = False
16
-
17
- def ai_advisory(report_text):
18
- """
19
- Produce a short narrative advisory using OpenAI if available.
20
- Advisory is intentionally non-actionable and high-level.
21
- """
22
- if not OPENAI_AVAILABLE:
23
- return "\n\n---\n*AI Advisory unavailable (OpenAI key not set).*"
24
- try:
25
- resp = openai.ChatCompletion.create(
26
- model="gpt-4o-mini",
27
- messages=[
28
- {"role": "system", "content": "You are an analyst that provides high-level, non-actionable organizational advice."},
29
- {"role": "user", "content": "Provide a concise, non-actionable advisory based on the following sanitized report:\n\n" + report_text}
30
- ],
31
- max_tokens=400
32
- )
33
- return "\n\n---\n### AI Narrative Advisory\n" + resp["choices"][0]["message"]["content"]
34
- except Exception as e:
35
- return f"\n\n---\n*AI Advisory failed: {str(e)}*"
36
 
37
- def is_affirmative(text):
38
- if text is None:
39
- return False
40
- return str(text).strip().lower() in ("yes", "y", "true", "1", "affirmative")
41
-
42
- # --- Short one-line questions (sanitized placeholders) ---
43
 
 
44
  STD_QUESTIONS = [
45
- "When/where enemy sighted?",
46
- "Direction of approach?",
47
- "Enemy size?",
48
- "Weapons carried?",
49
- "Vehicles or on foot?",
50
- "Distance to roads?",
51
- "Distance to camp?",
52
- "Distance to soldiers?",
53
- "Support from locals?",
54
- "Enemy disposition?",
55
- "Any R&S team nearby?",
56
- "Source of info?",
57
- "Info confirmed?",
58
- "Distance of own unit?",
59
- "Terrain type?"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  ]
61
 
62
- ENH_QUESTIONS = [
63
- "Ops & Int sections integrated?",
64
- "Unit has Int SOP?",
65
- "R&S plan exists?",
66
- "Force Protection SOP present?",
67
- "Forward Int projection?",
68
- "Vulnerability analysis tool?",
69
- "Randomness in movement?",
70
- "Source vetting & CI registry?",
71
- "Unit has Int doctrine?",
72
- "CI used in ops/base safety?",
73
- "Embedded Int in routine ops?",
74
- "Staff aligned with CO intent?",
75
- "Using 5D system?",
76
- "MI assets align with 5D?",
77
- "Red-teaming done?",
78
- "FP measures prioritized?",
79
- "Attack enemy SA/support?",
80
- "Deliberate or Quick ops ready?",
81
- "Projected C2 capability?",
82
- "Clear distinction SA/Surprise?"
83
  ]
84
 
85
- POSTMORTEM_QUESTIONS = [
86
- "Why enemy not detected?",
87
- "Why no HUMINT report?",
88
- "Why no R&S report?",
89
- "Why no FP report?",
90
- "Why no tactical C2 nodes?",
91
- "Why no target standards report?",
92
- "Why indicators not observed?",
93
- "Why no human terrain use?",
94
- "Why no terrain exploitation?",
95
- "Why no forward ops zones?"
96
  ]
97
 
98
- # --- Report Generators (sanitized, non-actionable) ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
 
100
- def generate_report(questions, *answers):
 
 
 
101
  """
102
- Create a sanitized report listing question labels and provided answers.
103
- Also returns a simple completeness percentage based on presence of answers.
104
  """
105
- lines = []
106
- for q, a in zip(questions, answers):
107
- lines.append(f"- {q}: {a if a else '[Not answered]'}")
108
- # completeness percentage
109
- present_count = sum(1 for a in answers if a and str(a).strip())
110
- pct = int((present_count / len(questions)) * 100) if questions else 0
111
- lines.append(f"\n**Completeness:** {pct}%")
112
- return "\n".join(lines)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
 
114
- def compute_readiness(std_answers, enh_answers, post_answers):
115
- total = len(STD_QUESTIONS) + len(ENH_QUESTIONS) + len(POSTMORTEM_QUESTIONS)
116
- answered = sum(1 for a in list(std_answers) + list(enh_answers) + list(post_answers) if a and str(a).strip())
117
- return int((answered / total) * 100) if total else 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
- def deficiency_advisory(std_answers, enh_answers, post_answers):
 
 
 
 
 
 
 
 
 
 
 
120
  """
121
- Produce a sanitized deficiency advisory listing missing fields and suggesting admin/training review.
122
- This is intentionally non-operational.
123
  """
124
- notes = []
125
- all_qs = STD_QUESTIONS + ENH_QUESTIONS + POSTMORTEM_QUESTIONS
126
- all_as = list(std_answers) + list(enh_answers) + list(post_answers)
127
- for q, a in zip(all_qs, all_as):
128
- if not a or not str(a).strip():
129
- notes.append(f"- Missing response: {q} Advisory: review documentation, training, and administrative processes.")
130
- return "\n".join(notes) if notes else "All placeholders have responses; proceed with formal review."
131
-
132
- COMMANDERS_GUIDE = """
133
- # Commander's Guide — WARN Levels (Sanitized)
134
- - 🟢 GREEN (85–100%): High completeness; routine reviews and training maintenance.
135
- - 🔵 BLUE (70–84%): Moderate completeness; targeted refresher training recommended.
136
- - 🟠 ORANGE (50–69%): Low completeness; initiate administrative audits and training.
137
- - 🔴 RED (<50%): Very low completeness; prioritize staff training, documentation, and policy remediation.
138
- """
139
-
140
- # --- Build Gradio UI ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
 
 
 
 
 
 
 
 
 
 
 
142
  with gr.Blocks() as demo:
143
- # Full banner (as requested)
144
- gr.HTML('<img src="https://huggingface.co/spaces/Militaryint/ops/resolve/main/banner.png" width="100%">')
145
- gr.Markdown("# 🟡 Kashmir AO Action Plan (Sanitized) — Analyst Tool & Battle Planner")
146
 
147
- # --- Standard Tab ---
148
- with gr.Tab("Standard"):
149
  std_inputs = [gr.Textbox(label=q, lines=1) for q in STD_QUESTIONS]
150
- std_button = gr.Button("Generate Standard Report")
151
- std_output = gr.Markdown()
152
- # click wiring: pass textbox values individually
153
- std_button.click(fn=lambda *a: generate_report(STD_QUESTIONS, *a), inputs=std_inputs, outputs=std_output)
154
-
155
- # --- Enhanced Tab ---
156
- with gr.Tab("Enhanced"):
157
- enh_inputs = [gr.Textbox(label=q, lines=1) for q in ENH_QUESTIONS]
158
- enh_button = gr.Button("Generate Enhanced Report")
159
- enh_output = gr.Markdown()
160
- enh_button.click(fn=lambda *a: generate_report(ENH_QUESTIONS, *a), inputs=enh_inputs, outputs=enh_output)
161
-
162
- # --- Postmortem Tab ---
163
- with gr.Tab("Postmortem"):
164
- post_inputs = [gr.Textbox(label=q, lines=1) for q in POSTMORTEM_QUESTIONS]
165
- post_button = gr.Button("Generate Postmortem Checklist")
166
- post_output = gr.Markdown()
167
- post_button.click(fn=lambda *a: generate_report(POSTMORTEM_QUESTIONS, *a), inputs=post_inputs, outputs=post_output)
168
-
169
- # --- Threat Readiness Tab ---
 
 
 
 
 
 
170
  with gr.Tab("Threat Readiness"):
171
- gr.Markdown("Evaluate overall completeness/readiness (sanitized). Toggle AI advisory to append a high-level narrative if OpenAI key is set in environment.")
172
- ai_toggle = gr.Checkbox(label="Append AI advisory (if OpenAI key set)", value=False)
173
- threat_button = gr.Button("Evaluate Readiness")
174
- threat_output = gr.Markdown()
175
- guide_output = gr.Markdown(COMMANDERS_GUIDE)
176
-
177
- def evaluate_readiness(*all_inputs):
178
- """
179
- all_inputs = std_inputs values + enh_inputs values + post_inputs values + [toggle_bool]
180
- """
181
- tot_std = len(STD_QUESTIONS)
182
- tot_enh = len(ENH_QUESTIONS)
183
- tot_post = len(POSTMORTEM_QUESTIONS)
184
- # parse
185
- std_vals = all_inputs[0:tot_std]
186
- enh_vals = all_inputs[tot_std:tot_std+tot_enh]
187
- post_vals = all_inputs[tot_std+tot_enh:tot_std+tot_enh+tot_post]
188
- toggle_val = all_inputs[-1]
189
- score = compute_readiness(std_vals, enh_vals, post_vals)
190
- if score >= 85:
191
- status = "🟢 GREEN"
192
- elif score >= 70:
193
- status = "🔵 BLUE"
194
- elif score >= 50:
195
- status = "🟠 ORANGE"
196
- else:
197
- status = "🔴 RED"
198
- base = f"# Readiness Score: {score}%\n**Status:** {status}"
199
- deficiencies = deficiency_advisory(std_vals, enh_vals, post_vals)
200
- combined = base + "\n\n" + deficiencies
201
- if toggle_val:
202
- combined = combined + ai_advisory(combined)
203
- return combined
204
-
205
- # flatten inputs when wiring the click event to avoid nested lists error
206
- threat_button.click(
207
- fn=evaluate_readiness,
208
- inputs=std_inputs + enh_inputs + post_inputs + [ai_toggle],
209
- outputs=threat_output
210
- )
211
-
212
- # --- Doctrinal Summary Tab ---
213
- with gr.Tab("Doctrinal Summary (Sanitized)"):
214
- gr.Markdown(
215
- "## Doctrinal Summary (Sanitized)\n\n"
216
- "- Treat intelligence as an operational function integrated into staff planning.\n"
217
- "- Aim for persistent situational awareness via C3ISR at an organizational level.\n"
218
- "- Use zoning (rear / forward / deep forward) conceptually for planning and resource allocation.\n"
219
- "- Maintain Force Protection SOPs, redundancy, and regular vulnerability reviews.\n"
220
- "- Emphasize training, documentation, and staff alignment with commander intent.\n\n"
221
- "_This doctrinal content is intentionally non-actionable and high-level._"
222
- )
223
-
224
- # --- Launch ---
225
  if __name__ == "__main__":
226
  demo.launch()
 
1
+ # app.py
2
+ # Safe KB-driven retrieval + report generator (NON-ACTIONABLE)
3
+ # - Loads PDFs/TXT/MD from /mnt/data
4
+ # - Builds chunk index (embedding or lexical fallback)
5
+ # - For each answer: retrieves top chunks relevant to that answer (the "lens")
6
+ # - Produces sanitized, administrative/doctrinal reports only (no tactical/operational content)
7
+ # - Threat readiness color-coded brief (lists deficiencies + admin remediations only)
8
+
9
  import os
10
+ import math
11
+ import textwrap
12
+ import re
13
+ from collections import defaultdict
14
  import gradio as gr
15
 
16
+ # Try modern OpenAI client (OpenAI()). If absent, client will be None and we fallback.
17
+ client = None
18
  try:
19
+ from openai import OpenAI
20
+ client = OpenAI()
 
 
 
 
 
21
  except Exception:
22
+ client = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
+ BANNER_URL = "https://huggingface.co/spaces/Militaryint/ops/resolve/main/banner.png"
 
 
 
 
 
25
 
26
+ # ---------- QUESTION SETS (exact phrasing provided) ----------
27
  STD_QUESTIONS = [
28
+ "When where was enemy sighted.",
29
+ "Coming from which direction",
30
+ "what is the size of the enemy? How many men?",
31
+ "What equipment and weapons are they carrying",
32
+ "What vehicles are they using or are they on foot?",
33
+ "How far are they from any roads frequented by soldiers vehicles",
34
+ "How far are they from any military unit camp",
35
+ "How far are they from any deployed soldiers",
36
+ "Are they getting support of locals? If so who are these locals?",
37
+ "What is their disposition? How are they spread out in formation? Are they moving closely tight or are they spread out?",
38
+ "Do you as the security force commander have Reconnaissance and Surveillance soldiers near the area where these enemy sighted?",
39
+ "Did you get the information of sighting of enemy from local source or army personnel?",
40
+ "If you got information from local source did you confirm.that information from second source or by sending your Reconnaissance and Surveillance team to observe and confirm truth of what source said? Please remember source can be planted by enemy to provide false information in order to lead your security forces to an ambush site of the enemy",
41
+ "How far is your commanded army unit from the place where enemy is sighted?",
42
+ "is the terrain urban or semi urban or jungle or hilly or rural?"
43
+ ]
44
+
45
+ ENH_SECTION_A = [
46
+ "Does the Bn have separate Ops planning and Int sections? Or do both have a common nodal point,for example an officer or JCO conversant with both Ops and Int.( Integration).",
47
+ "Does the Unit have int SOP? Is there any Course of Action template with provision for int feeds? Is there any Ops template?Ops SOP?",
48
+ "Does the unit have a proper reconnaissance and surveillance plan?Is it integrated with Ops and Int SOP?",
49
+ "Does the unit have Force Protection SOP? Threat Conditions Levels?FP specific route planner? Base FP review capability denoted by intelligence and counterintelligence vulnerability reports?",
50
+ "Does the unit have intelligence projection capability?In shape of forward intelligence surveillance and reconnaissance nodes,covert and regular? These nodes are one or two manned emplacements outside base in specific sectors of the AOR? May even comprise small teams.Intelligence projection is required so that these cells can be activated any time on receipt of info say enemy movement in nearby areas of the sector of AOR where they are emplaced.",
51
+ "Is there a vulnerablity analysis tool for unit? For example to determine vulnerable assets and critical assets.the former if compromised will not halt unit ops and mission but the latter will.The physical layout of camp assets for example should be out of reach of intrusive action like a breach of camp perimeter or attack on security gate.",
52
+ "Does the unit employ Randomness in guarding,troops movement and officers movement? Randomness is necessary to thwart enemy surveillance.",
53
+ "Is there a source vetting system in place? Is counterintelligence used to confirm source levels of efficiency and trust? Does the unit have a source registry?",
54
+ "Does the unit treat intelligence as only sourced information or as a repository of different tactics and techniques? Is there any intelligence doctrine or manual in unit library detailing these tactics?",
55
+ "Does the unit use counterintelligence in vulnerability analysis of ops and base safety? Is it understood that CI is not only spying but is also a repository of intelligence tactics amended to attack enemy intent and enemy situational Awareness?Does the unit int section have provisions to employ force protection in terms of intelligence and CI and not just use static physical protection methods and routine convoy protection?",
56
+ "Does the unit have embedded int personnel in routine ops like checkpoint,road blocks,cordon and search,patrols and R&S teams? This facilitates screening and advance Warn."
57
  ]
58
 
59
+ ENH_SECTION_B = [
60
+ "Am I thinking of the Threat or am I thinking about my COs Situational Awareness.",
61
+ "What is my intent as Staff planning element?Do i align myself with Cdrs Intent or do I point out all the alternatives not apparent to him?",
62
+ "Do I detect the enemy or do I deter him from hostile action,or do i deny him avenues of hostile action and intelligence about our activities and plans,or do I deliver the locals from his influence or do I plan to outright destroy him physically.The 5Ds.",
63
+ "My unit has no organic intelligence assets.I have to depend on sources and inputs from MI units.Do their way of working conform to 5D system?Can these assets get me information in line with 5D system requirements?Can I use my Bn soldiers somehow to work as per 5D system?",
64
+ "Using two guide rails of 5D system,viz Deter and Deny did i make out a Vulnerability Assessment of my Bn security measures and information security both at base,during routine ops and deployed areas of troops outside base?Did I conduct a red teaming on these two guide rails as premise?",
65
+ "How do I account for Force Protection based on Q5?Am I giving FP top priority?",
66
+ "Do we attack the threats situational awareness,or his freedom of movement,or his tactics or his local support? Do i have the intelligence capability to assess on these lines?",
67
+ "Is the call for operation Deliberate or Quick?Do I have the appropriate int and R&S assets to report in both cases? In Quick mode do I have projected intelligence capability,that is do I have covert or overt int and R&S assets emplaced near or at the area of reported threat activity? If not how do I make an on the spot threat assessment before launching ops? So as to avert Surprise?",
68
+ "Does my Unit have a projected command and control capability in the AOR outside HQs?Does it have emplaced covert and overt tactical C2 nodes in focussed areas of AOR? Is there Net of troops and R&S plus int assets in touch with these tactical C2 nodes?",
69
+ "Do I clearly distinguish between Advance Warn, Surprise and Situational Awareness? Do i accept the fact that all intelligence collected by MI and sources may be accurate but SA wrong leading to failure? Do i understand that even SA can be rendered wrong by Enemy Surprise?"
 
 
 
 
 
 
 
 
 
 
70
  ]
71
 
72
+ ENH_SECTION_C = [
73
+ "Why was not the enemy's movement detected beforehand?",
74
+ "Why was there no HUMINT report on locals information about possibility of an attack?",
75
+ "Why was there no reconnaissance and surveillance report?",
76
+ "Why was there no advance Force Protection report?",
77
+ "Why was there no Tactical Command and Control nodes covertly emplaced in points equidistant from vulnerable targets like schools?",
78
+ "Why was there no Terrorist Targeting Standards Report that will predict likely targets of terrorist attack?",
79
+ "Why did the Army fail to observe indicators?",
80
+ "Why did the Army fail to exploit human terrain for advance information?",
81
+ "Why was there no exploitation of Physical terrain?",
82
+ "Why was forward operating zones absent?"
83
  ]
84
 
85
+ # -------------------------
86
+ # Utilities: read PDFs / files
87
+ # -------------------------
88
+ def read_all_files(data_dir="/mnt/data"):
89
+ files_text = {}
90
+ if not os.path.isdir(data_dir):
91
+ return files_text
92
+ for fname in sorted(os.listdir(data_dir)):
93
+ path = os.path.join(data_dir, fname)
94
+ if not os.path.isfile(path):
95
+ continue
96
+ lower = fname.lower()
97
+ try:
98
+ if lower.endswith(".pdf"):
99
+ try:
100
+ import PyPDF2
101
+ with open(path, "rb") as f:
102
+ reader = PyPDF2.PdfReader(f)
103
+ pages = []
104
+ for p in reader.pages:
105
+ try:
106
+ t = p.extract_text()
107
+ if t:
108
+ pages.append(t)
109
+ except Exception:
110
+ continue
111
+ files_text[fname] = "\n\n".join(pages) if pages else "[PDF read but no extractable text]"
112
+ except Exception as e:
113
+ files_text[fname] = f"[Could not read PDF: {e}]"
114
+ elif lower.endswith(".txt") or lower.endswith(".md"):
115
+ try:
116
+ with open(path, "r", encoding="utf-8", errors="ignore") as f:
117
+ files_text[fname] = f.read()
118
+ except Exception as e:
119
+ files_text[fname] = f"[Could not read text file: {e}]"
120
+ else:
121
+ continue
122
+ except Exception:
123
+ files_text[fname] = "[Error reading file]"
124
+ return files_text
125
+
126
+ # -------------------------
127
+ # Chunking helper
128
+ # -------------------------
129
+ def chunk_text(text, max_chars=2000, overlap=200):
130
+ if not text:
131
+ return []
132
+ chunks = []
133
+ start = 0
134
+ length = len(text)
135
+ while start < length:
136
+ end = min(start + max_chars, length)
137
+ chunk = text[start:end]
138
+ chunks.append(chunk)
139
+ if end == length:
140
+ break
141
+ start = end - overlap
142
+ return chunks
143
 
144
+ # -------------------------
145
+ # Embedding / similarity (with fallback)
146
+ # -------------------------
147
+ def get_embedding(text):
148
  """
149
+ Returns an embedding vector for text using OpenAI embeddings if available.
150
+ If no client is available, returns None.
151
  """
152
+ if client is None:
153
+ return None
154
+ try:
155
+ resp = client.embeddings.create(model="text-embedding-3-small", input=text)
156
+ # handle SDK variations
157
+ try:
158
+ return resp["data"][0]["embedding"]
159
+ except Exception:
160
+ return resp.data[0].embedding
161
+ except Exception:
162
+ return None
163
+
164
+ def cosine_sim(a, b):
165
+ # both lists of floats
166
+ if a is None or b is None:
167
+ return 0.0
168
+ dot = sum(x*y for x,y in zip(a,b))
169
+ na = math.sqrt(sum(x*x for x in a))
170
+ nb = math.sqrt(sum(x*x for x in b))
171
+ if na == 0 or nb == 0:
172
+ return 0.0
173
+ return dot / (na*nb)
174
+
175
+ # -------------------------
176
+ # Build in-memory index of chunks
177
+ # -------------------------
178
+ class KBIndex:
179
+ def __init__(self):
180
+ self.chunks = [] # list of dicts: {file, chunk_id, text, emb}
181
+ def build_from_files(self, files_text):
182
+ self.chunks = []
183
+ for fname, text in files_text.items():
184
+ for i, chunk in enumerate(chunk_text(text, max_chars=2000, overlap=300)):
185
+ emb = get_embedding(chunk) # may be None
186
+ self.chunks.append({"file": fname, "id": f"{fname}::chunk{i+1}", "text": chunk, "emb": emb})
187
+ def retrieve(self, query, top_k=3):
188
+ # prefer embedding similarity when available, else lexical score
189
+ q_emb = get_embedding(query) if client else None
190
+ scored = []
191
+ if q_emb is not None:
192
+ for c in self.chunks:
193
+ score = cosine_sim(q_emb, c["emb"]) if c["emb"] is not None else 0.0
194
+ scored.append((score, c))
195
+ scored.sort(key=lambda x: x[0], reverse=True)
196
+ return [c for s,c in scored[:top_k] if s>0]
197
+ else:
198
+ # lexical fallback: simple token overlap score
199
+ q_tokens = set(re.findall(r"\w+", query.lower()))
200
+ for c in self.chunks:
201
+ tokens = set(re.findall(r"\w+", c["text"].lower()))
202
+ score = len(q_tokens & tokens)
203
+ scored.append((score, c))
204
+ scored.sort(key=lambda x: x[0], reverse=True)
205
+ return [c for s,c in scored[:top_k] if s>0]
206
 
207
+
208
+
209
+ def generate_admin_alternative(original_text):
210
+ # Produce a generic admin-oriented alternative (deterministic)
211
+ alt = (
212
+ "Administrative alternative (examples):\n"
213
+ "- Update SOP and reporting templates to include required fields.\n"
214
+ "- Conduct doctrinal review and table-top exercises; document lessons learned.\n"
215
+ "- Implement source vetting and CI audit trails; schedule training and compliance checks.\n"
216
+ "- Commission a vulnerability assessment and a red-team *administrative* review (no operational details).\n"
217
+ )
218
+ return alt
219
+
220
+ # -------------------------
221
+ # Chat API wrapper for completions
222
+ # -------------------------
223
+ def call_chat_api(messages, max_tokens=900, model="gpt-4o-mini"):
224
+ if client is None:
225
+ raise RuntimeError("OpenAI client not available in environment.")
226
+ resp = client.chat.completions.create(model=model, messages=messages, max_tokens=max_tokens)
227
+ try:
228
+ return resp["choices"][0]["message"]["content"]
229
+ except Exception:
230
+ return resp.choices[0].message.content
231
 
232
+ # -------------------------
233
+ # Prompt templates (strictly NON-ACTIONABLE)
234
+ # -------------------------
235
+ SYSTEM_SAFE = (
236
+ "You are an institutional analyst. You MUST follow these rules: "
237
+ "1) Provide ONLY non-actionable, administrative, doctrinal or training-oriented analysis. "
238
+ "2) If asked for operational content, refuse and instead produce administrative alternatives (SOPs, audits, training, doctrine). "
239
+ "3) Cite the KB chunk ids / filenames you used in reasoning. "
240
+ "Output must be explicit that it's NON-ACTIONABLE."
241
+ )
242
+
243
+ def build_standard_prompt(answer_map, retrieved_chunks_for_answers):
244
  """
245
+ Build messages: system + user with attached relevant KB excerpts per answer.
246
+ retrieved_chunks_for_answers: list of lists of chunk dicts in same order as answers
247
  """
248
+ user_parts = []
249
+ user_parts.append("Produce a NON-ACTIONABLE Standard Advisory Report for a commander.")
250
+ user_parts.append("Include: Executive Summary (2-4 lines); Key Administrative Issues (bulleted); Prioritized Administrative Recommendations (numbered); Sources cited (file::chunk ids).")
251
+ user_parts.append("DO NOT include any tactical or operational instructions.")
252
+ user_parts.append("\nAnswers and relevant KB excerpts (lens):\n")
253
+ for i, q in enumerate(STD_QUESTIONS):
254
+ ans = answer_map.get(q, "[no answer]")
255
+ user_parts.append(f"Q{i+1}. {q}\nA: {ans}\nRelevant KB excerpts:")
256
+ chunks = retrieved_chunks_for_answers[i]
257
+ if chunks:
258
+ for c in chunks:
259
+ user_parts.append(f"--- {c['id']} (from {c['file']}) ---\n{c['text'][:1200]}\n")
260
+ else:
261
+ user_parts.append("- [No relevant KB excerpt found]\n")
262
+ user_text = "\n".join(user_parts)
263
+ return [{"role":"system","content":SYSTEM_SAFE}, {"role":"user","content":user_text}]
264
+
265
+ def build_enhanced_prompt(a_map, b_map, c_map, gate_text, retrieved_A, retrieved_B, retrieved_C):
266
+ user_parts = []
267
+ user_parts.append("Produce a NON-ACTIONABLE Enhanced Staff Advisory for a commander.")
268
+ user_parts.append("Include: Executive Summary; Key doctrinal/organizational gaps; Prioritized Administrative Remediations; Measurement/audit suggestions; Sources cited.")
269
+ user_parts.append("\nSection A answers and relevant KB excerpts:\n")
270
+ for i, q in enumerate(ENH_SECTION_A):
271
+ ans = a_map.get(q, "[no answer]")
272
+ user_parts.append(f"A{i+1}. {q}\nA: {ans}\nRelevant KB excerpts:")
273
+ chunks = retrieved_A[i]
274
+ if chunks:
275
+ for c in chunks:
276
+ user_parts.append(f"--- {c['id']} (from {c['file']}) ---\n{c['text'][:1200]}\n")
277
+ else:
278
+ user_parts.append("- [No relevant KB excerpt found]\n")
279
+ user_parts.append("\nSection B answers and relevant KB excerpts:\n")
280
+ for i, q in enumerate(ENH_SECTION_B):
281
+ ans = b_map.get(q, "[no answer]")
282
+ user_parts.append(f"B{i+1}. {q}\nA: {ans}\nRelevant KB excerpts:")
283
+ chunks = retrieved_B[i]
284
+ if chunks:
285
+ for c in chunks:
286
+ user_parts.append(f"--- {c['id']} (from {c['file']}) ---\n{c['text'][:1200]}\n")
287
+ else:
288
+ user_parts.append("- [No relevant KB excerpt found]\n")
289
+ user_parts.append("\nSection C answers and relevant KB excerpts:\n")
290
+ for i, q in enumerate(ENH_SECTION_C):
291
+ ans = c_map.get(q, "[no answer]")
292
+ user_parts.append(f"C{i+1}. {q}\nA: {ans}\nRelevant KB excerpts:")
293
+ chunks = retrieved_C[i]
294
+ if chunks:
295
+ for c in chunks:
296
+ user_parts.append(f"--- {c['id']} (from {c['file']}) ---\n{c['text'][:1200]}\n")
297
+ else:
298
+ user_parts.append("- [No relevant KB excerpt found]\n")
299
+ user_parts.append(f"\nGate assessment: {gate_text}\n")
300
+ user_text = "\n".join(user_parts)
301
+ return [{"role":"system","content":SYSTEM_SAFE}, {"role":"user","content":user_text}]
302
+
303
+ # -------------------------
304
+ # Deterministic remediation helpers (as earlier) - administrative only
305
+ # -------------------------
306
+ def remediation_for_std_question(q_text):
307
+ q = q_text.lower()
308
+ rem = []
309
+ if "sighted" in q or "where" in q:
310
+ rem.append("Ensure SITREP templates include precise time and geo fields and audit entries for accuracy.")
311
+ if "direction" in q:
312
+ rem.append("Add directional/axis fields to reporting templates and train observers.")
313
+ if "size of the enemy" in q or "how many" in q:
314
+ rem.append("Include size estimation procedures in SOP and require cross-checks.")
315
+ if "equipment" in q or "weapons" in q:
316
+ rem.append("Standardize equipment reporting taxonomy in the unit KB and train observers.")
317
+ if "vehicles" in q:
318
+ rem.append("Include vehicle identification fields in R&S templates and verify via secondary observation.")
319
+ if "roads" in q:
320
+ rem.append("Map and flag critical infrastructure in vulnerability audits.")
321
+ if "locals" in q:
322
+ rem.append("Apply CI vetting to local-source reports and require secondary confirmation.")
323
+ if "disposition" in q:
324
+ rem.append("Document disposition notation standards and rehearse via table-top exercises.")
325
+ if "reconnaissance" in q or "r&s" in q:
326
+ rem.append("Maintain an on-call R&S roster and ensure cover/concealment SOPs are current.")
327
+ if "confirmed" in q or "second source" in q:
328
+ rem.append("Mandate secondary confirmation and log confirmations in a secure registry.")
329
+ if "terrain" in q:
330
+ rem.append("Ensure terrain classification fields are mandatory and staff have access to terrain overlays.")
331
+ if not rem:
332
+ rem.append("Review reporting templates and conduct staff training to improve data capture.")
333
+ seen = []
334
+ for r in rem:
335
+ if r not in seen:
336
+ seen.append(r)
337
+ return seen
338
+
339
+ def remediation_for_enh_question(q_text):
340
+ q = q_text.lower()
341
+ rem = []
342
+ if "ops and int" in q or "integration" in q:
343
+ rem.append("Establish Ops-Int coordination SOP and a liaison role with documented responsibilities.")
344
+ if "int sop" in q or "course of action" in q:
345
+ rem.append("Create Intelligence SOP and COA templates that include int feeds and validation steps.")
346
+ if "reconnaissance and surveillance plan" in q:
347
+ rem.append("Publish R&S plan templates and run table-top exercises to validate them.")
348
+ if "force protection" in q or "route planner" in q:
349
+ rem.append("Institute FP review cycles and formal route planning checklists for missions and base movement.")
350
+ if "intelligence projection" in q or "forward intelligence" in q:
351
+ rem.append("Document intelligence projection concept, minimum manning and activation triggers in SOP.")
352
+ if "vulnerability analysis" in q:
353
+ rem.append("Adopt a vulnerability register and schedule regular audits and red-team reviews (administrative).")
354
+ if "randomness" in q:
355
+ rem.append("Implement randomized movement policies and document patterns for audit (ensure safety/legal compliance).")
356
+ if "source vetting" in q or "counterintelligence" in q:
357
+ rem.append("Build a source registry and CI vetting workflow with secure access and audit logs.")
358
+ if "doctrine" in q or "manual" in q:
359
+ rem.append("Compile and publish an internal intelligence doctrine/manual in the unit library.")
360
+ if "embedded int" in q:
361
+ rem.append("Define embedded-int roles, responsibilities and rotation cycles; maintain oversight.")
362
+ if not rem:
363
+ rem.append("Conduct a formal capability gap analysis and document required SOPs and resourcing.")
364
+ seen = []
365
+ for r in rem:
366
+ if r not in seen:
367
+ seen.append(r)
368
+ return seen
369
+
370
+ # -------------------------
371
+ # Main report functions
372
+ # -------------------------
373
+ def generate_standard_report_with_lens(answers_map, kb_index, top_k_per_answer=2):
374
+ # for each STD question, retrieve top_k_per_answer chunks
375
+ retrieved = []
376
+ for q in STD_QUESTIONS:
377
+ ans = answers_map.get(q, "")
378
+ query_text = (str(ans).strip() or q)
379
+ chunks = kb_index.retrieve(query_text, top_k=top_k_per_answer)
380
+ retrieved.append(chunks)
381
+ # build prompt
382
+ messages = build_standard_prompt(answers_map, retrieved)
383
+ try:
384
+ out = call_chat_api(messages, max_tokens=1200)
385
+ out = safety_check_and_redact(out)
386
+ return out
387
+ except Exception as e:
388
+ # fallback deterministic: detailed administrative report referencing retrieved chunks
389
+ lines = ["[FALLBACK NON-ACTIONABLE STANDARD REPORT]\n"]
390
+ for i, q in enumerate(STD_QUESTIONS):
391
+ a = answers_map.get(q, "")
392
+ lines.append(f"Q{i+1}. {q}\nA: {a}\nRelevant KB excerpts (ids):")
393
+ chunks = retrieved[i]
394
+ if chunks:
395
+ for c in chunks:
396
+ lines.append(f"- {c['id']} (from {c['file']})")
397
+ else:
398
+ lines.append("- [None found]")
399
+ # remediation per negative answer
400
+ if not a or str(a).strip().lower() in ("", "no", "n", "none", "negative"):
401
+ for r in remediation_for_std_question(q):
402
+ lines.append(f" *Admin remediation:* {r}")
403
+ lines.append("")
404
+ lines.append(f"\nNote: AI unavailable: {e}")
405
+ return "\n".join(lines)
406
+
407
+ def generate_enhanced_report_with_lens(a_map, b_map, c_map, gate_text, kb_index, top_k_per_answer=2):
408
+ retrieved_A = [kb_index.retrieve(str(a_map.get(q,"") or q), top_k=top_k_per_answer) for q in ENH_SECTION_A]
409
+ retrieved_B = [kb_index.retrieve(str(b_map.get(q,"") or q), top_k=top_k_per_answer) for q in ENH_SECTION_B]
410
+ retrieved_C = [kb_index.retrieve(str(c_map.get(q,"") or q), top_k=top_k_per_answer) for q in ENH_SECTION_C]
411
+ messages = build_enhanced_prompt(a_map, b_map, c_map, gate_text, retrieved_A, retrieved_B, retrieved_C)
412
+ try:
413
+ out = call_chat_api(messages, max_tokens=1400)
414
+ out = safety_check_and_redact(out)
415
+ return out
416
+ except Exception as e:
417
+ # fallback deterministic enhanced report
418
+ lines = ["[FALLBACK NON-ACTIONABLE ENHANCED REPORT]\n"]
419
+ lines.append("Section A gaps and admin remediations:")
420
+ for q in ENH_SECTION_A:
421
+ a = a_map.get(q,"")
422
+ if not a or str(a).strip().lower() in ("","no","n","none","negative"):
423
+ lines.append(f"- {q}")
424
+ for r in remediation_for_enh_question(q):
425
+ lines.append(f" *Admin remediation:* {r}")
426
+ lines.append("\nSection B gaps and admin remediations:")
427
+ for q in ENH_SECTION_B:
428
+ b = b_map.get(q,"")
429
+ if not b or str(b).strip().lower() in ("","no","n","none","negative"):
430
+ lines.append(f"- {q}")
431
+ for r in remediation_for_enh_question(q):
432
+ lines.append(f" *Admin remediation:* {r}")
433
+ lines.append("\nSection C gaps and admin remediations:")
434
+ for q in ENH_SECTION_C:
435
+ c = c_map.get(q,"")
436
+ if not c or str(c).strip().lower() in ("","no","n","none","negative"):
437
+ lines.append(f"- {q}")
438
+ for r in remediation_for_enh_question(q):
439
+ lines.append(f" *Admin remediation:* {r}")
440
+ lines.append(f"\nNote: AI unavailable: {e}")
441
+ return "\n".join(lines)
442
+
443
+ # -------------------------
444
+ # Threat readiness (color-coded) with deficiency listing
445
+ # -------------------------
446
+ def evaluate_threat_brief(*all_answers):
447
+ total = len(STD_QUESTIONS) + len(ENH_SECTION_A) + len(ENH_SECTION_B) + len(ENH_SECTION_C)
448
+ vals = list(all_answers)[:total] + [""] * max(0, total - len(all_answers))
449
+ yes_count = sum(1 for v in vals if v and str(v).strip().lower() in ("yes","y","true","1","affirmative","confirmed"))
450
+ readiness = int((yes_count / total) * 100) if total else 0
451
+ if readiness >= 85:
452
+ color = "🟢 GREEN"
453
+ meaning = "Peacetime / strong readiness"
454
+ elif readiness >= 70:
455
+ color = "🔵 BLUE"
456
+ meaning = "Anticipated short-term threat readiness"
457
+ elif readiness >= 50:
458
+ color = "🟠 ORANGE"
459
+ meaning = "Imminent threat readiness (admin gaps present)"
460
+ else:
461
+ color = "🔴 RED"
462
+ meaning = "Low readiness - significant administrative gaps"
463
+
464
+ # collect deficiencies and remediations
465
+ deficiencies = []
466
+ rems = []
467
+ idx = 0
468
+ for q in STD_QUESTIONS:
469
+ a = vals[idx]; idx+=1
470
+ if not a or str(a).strip().lower() in ("","no","n","none","negative"):
471
+ deficiencies.append(f"STD: {q}")
472
+ rems.extend(remediation_for_std_question(q))
473
+ for q in ENH_SECTION_A:
474
+ a = vals[idx]; idx+=1
475
+ if not a or str(a).strip().lower() in ("","no","n","none","negative"):
476
+ deficiencies.append(f"ENH A: {q}")
477
+ rems.extend(remediation_for_enh_question(q))
478
+ for q in ENH_SECTION_B:
479
+ a = vals[idx]; idx+=1
480
+ if not a or str(a).strip().lower() in ("","no","n","none","negative"):
481
+ deficiencies.append(f"ENH B: {q}")
482
+ rems.extend(remediation_for_enh_question(q))
483
+ for q in ENH_SECTION_C:
484
+ a = vals[idx]; idx+=1
485
+ if not a or str(a).strip().lower() in ("","no","n","none","negative"):
486
+ deficiencies.append(f"ENH C: {q}")
487
+ rems.extend(remediation_for_enh_question(q))
488
+
489
+ # build concise commander brief (non-actionable)
490
+ lines = []
491
+ lines.append(f"# Threat Readiness: {readiness}%")
492
+ lines.append(f"**Status:** {color} — {meaning}\n")
493
+ lines.append("## Commander’s Guide to WARN Levels:")
494
+ lines.append("- 🔴 RED (<50%): Significant administrative deficiencies. Prioritize SOP, CI, and audits.")
495
+ lines.append("- 🟠 ORANGE (50–69%): Moderate deficiencies; strengthen doctrine & R&S validation.")
496
+ lines.append("- 🔵 BLUE (70–84%): Minor gaps; schedule audits and training.")
497
+ lines.append("- 🟢 GREEN (85–100%): Good status; maintain vigilance and periodic reviews.\n")
498
+
499
+ if deficiencies:
500
+ lines.append("## Identified Administrative Deficiencies (summary)")
501
+ for d in deficiencies[:40]:
502
+ lines.append(f"- {d}")
503
+ if len(deficiencies)>40:
504
+ lines.append(f"... and {len(deficiencies)-40} more.")
505
+ else:
506
+ lines.append("## Identified Administrative Deficiencies: None found.")
507
+
508
+ lines.append("\n## Example Administrative Remediations (prioritized)")
509
+ unique_rems = []
510
+ for r in rems:
511
+ if r not in unique_rems:
512
+ unique_rems.append(r)
513
+ if unique_rems:
514
+ for i, r in enumerate(unique_rems[:40],1):
515
+ lines.append(f"{i}. {r}")
516
+ else:
517
+ lines.append("- No admin remediations needed based on current answers.")
518
+ lines.append("\n**Note:** This brief is NON-ACTIONABLE. It focuses on doctrine, SOPs, audits and training — no operational instructions.")
519
+ return "\n".join(lines)
520
 
521
+ # -------------------------
522
+ # Build index at startup (best-effort)
523
+ # -------------------------
524
+ FILES_TEXT = read_all_files("/mnt/data")
525
+ KB_INDEX = KBIndex()
526
+ KB_INDEX.build_from_files(FILES_TEXT)
527
+
528
+ # -------------------------
529
+ # Gradio UI
530
+ # -------------------------
531
  with gr.Blocks() as demo:
532
+ gr.HTML(f'<img src="{BANNER_URL}" width="100%">')
533
+ gr.Markdown("# Kashmir AO Action Plan — KB-driven, Sanitized Adviser")
 
534
 
535
+ with gr.Tab("Standard Report"):
 
536
  std_inputs = [gr.Textbox(label=q, lines=1) for q in STD_QUESTIONS]
537
+ std_button = gr.Button("Generate Standard Report (sanitized + KB-lens)")
538
+ std_output = gr.Textbox(label="Standard Report (sanitized)", lines=24)
539
+ def std_runner(*answers):
540
+ amap = dict(zip(STD_QUESTIONS, answers))
541
+ return generate_standard_report_with_lens(amap, KB_INDEX, top_k_per_answer=3)
542
+ std_button.click(std_runner, inputs=std_inputs, outputs=std_output)
543
+
544
+ with gr.Tab("Enhanced Report"):
545
+ a_inputs = [gr.Textbox(label=q, lines=1) for q in ENH_SECTION_A]
546
+ b_inputs = [gr.Textbox(label=q, lines=1) for q in ENH_SECTION_B]
547
+ c_inputs = [gr.Textbox(label=q, lines=1) for q in ENH_SECTION_C]
548
+ gate_input = gr.Textbox(label="Gate Assessment (final note)", lines=1)
549
+ enh_button = gr.Button("Generate Enhanced Report (sanitized + KB-lens)")
550
+ enh_output = gr.Textbox(label="Enhanced Report (sanitized)", lines=28)
551
+ def enh_runner(*answers):
552
+ la = len(ENH_SECTION_A); lb = len(ENH_SECTION_B); lc = len(ENH_SECTION_C)
553
+ al = list(answers)
554
+ while len(al) < (la+lb+lc+1):
555
+ al.append("")
556
+ a_vals = al[:la]; b_vals = al[la:la+lb]; c_vals = al[la+lb:la+lb+lc]; gate_val = al[-1]
557
+ a_map = dict(zip(ENH_SECTION_A, a_vals))
558
+ b_map = dict(zip(ENH_SECTION_B, b_vals))
559
+ c_map = dict(zip(ENH_SECTION_C, c_vals))
560
+ return generate_enhanced_report_with_lens(a_map, b_map, c_map, gate_val, KB_INDEX, top_k_per_answer=3)
561
+ enh_button.click(enh_runner, inputs=a_inputs + b_inputs + c_inputs + [gate_input], outputs=enh_output)
562
+
563
  with gr.Tab("Threat Readiness"):
564
+ gr.Markdown("## Threat Readiness color-coded commander brief (administrative only)")
565
+
566
+ # Hidden states to capture answers from other tabs
567
+ std_state = gr.State()
568
+ a_state = gr.State()
569
+ b_state = gr.State()
570
+ c_state = gr.State()
571
+
572
+ threat_button = gr.Button("Evaluate Threat Readiness")
573
+ threat_output = gr.Textbox(label="Threat Readiness & Admin Diagnostics", lines=28)
574
+
575
+ # Capture answers whenever reports are generated
576
+ def cache_std(*answers):
577
+ return list(answers)
578
+ std_button.click(cache_std, inputs=std_inputs, outputs=[std_state])
579
+
580
+ def cache_enh(*answers):
581
+ la, lb, lc = len(ENH_SECTION_A), len(ENH_SECTION_B), len(ENH_SECTION_C)
582
+ a_vals = answers[:la]
583
+ b_vals = answers[la:la+lb]
584
+ c_vals = answers[la+lb:la+lb+lc]
585
+ return a_vals, b_vals, c_vals
586
+ enh_button.click(cache_enh, inputs=a_inputs+b_inputs+c_inputs+[gate_input],
587
+ outputs=[a_state, b_state, c_state])
588
+
589
+ def threat_runner(std_ans, a_ans, b_ans, c_ans):
590
+ # flatten all answers
591
+ all_vals = (std_ans or []) + (a_ans or []) + (b_ans or []) + (c_ans or [])
592
+ return evaluate_threat_brief(*all_vals)
593
+
594
+ threat_button.click(threat_runner, inputs=[std_state, a_state, b_state, c_state], outputs=threat_output)
595
+
596
+
597
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
598
  if __name__ == "__main__":
599
  demo.launch()