Seth0330 commited on
Commit
48ac593
·
verified ·
1 Parent(s): 1489f4b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +88 -250
app.py CHANGED
@@ -1,8 +1,12 @@
1
  import streamlit as st
2
  import os
3
  import json
4
- import requests
5
  import traceback
 
 
 
 
 
6
 
7
  # --- ALWAYS INIT SESSION STATE FIRST (before any widgets)
8
  if "json_data" not in st.session_state:
@@ -14,284 +18,118 @@ if "temp_input" not in st.session_state:
14
  if "files_loaded" not in st.session_state:
15
  st.session_state.files_loaded = False
16
 
17
- # --- Page config
18
- st.set_page_config(page_title="JSON-Backed AI Chat Agent", layout="wide")
19
- st.title("JSON-Backed AI Chat Agent")
20
-
21
- # --- Load API key
22
- OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
23
- if not OPENAI_API_KEY:
24
- st.error("❌ OPENAI_API_KEY not set in Settings → Secrets.")
25
- st.stop()
26
-
27
- HEADERS = {
28
- "Authorization": f"Bearer {OPENAI_API_KEY}",
29
- "Content-Type": "application/json",
30
- }
31
 
 
32
  st.sidebar.header("Upload Multiple JSON Files")
33
  uploaded_files = st.sidebar.file_uploader(
34
  "Choose one or more JSON files", type="json", accept_multiple_files=True
35
  )
36
 
37
- # --- Only clear/load state when files change
38
  if uploaded_files and not st.session_state.files_loaded:
39
  st.session_state.json_data.clear()
40
- file_summaries = []
41
  for f in uploaded_files:
42
  try:
43
  content = json.load(f)
44
  st.session_state.json_data[f.name] = content
45
- if isinstance(content, dict):
46
- keys = list(content.keys())
47
- elif isinstance(content, list) and content and isinstance(content[0], dict):
48
- keys = list(content[0].keys())
49
- else:
50
- keys = []
51
- file_summaries.append(f"{f.name}: keys={keys[:10]}{'...' if len(keys)>10 else ''}")
52
  st.sidebar.success(f"Loaded: {f.name}")
53
- st.sidebar.write(f"Keys: {keys[:10]}{'...' if len(keys)>10 else ''}")
54
  except Exception as e:
55
  st.sidebar.error(f"Error reading {f.name}: {e}")
56
-
57
- # --- System prompt with explicit few-shot examples
58
- system_message = {
59
- "role": "system",
60
- "content": (
61
- "You are an AI data analyst for uploaded JSON files. "
62
- "Each file may have different structures and keys, including lists and nested dictionaries. "
63
- "You have access to a function 'search_all_jsons' that finds all records in all JSON files where a key matches a value, recursively. "
64
- "If a user asks about groups of people or wants to know counts such as 'How many females are there?', "
65
- "interpret this as 'search for all records where gender equals female'. "
66
- "Always use the 'search_all_jsons' function with key='gender' and value='female' for such queries, unless another key/value is clear from context. "
67
- "If someone asks 'How many males?', search for gender equals male, etc. "
68
- "EXAMPLES:\n"
69
- "User: How many females are there?\n"
70
- "Assistant: (Call search_all_jsons with key='gender', value='female')\n"
71
- "User: How many males are there?\n"
72
- "Assistant: (Call search_all_jsons with key='gender', value='male')\n"
73
- "User: Show all females\n"
74
- "Assistant: (Call search_all_jsons with key='gender', value='female')\n"
75
- "User: How many people are named Emily?\n"
76
- "Assistant: (Call search_all_jsons with key='firstName', value='Emily')"
77
- )
78
- }
79
- st.session_state.messages = [system_message]
80
  st.session_state.files_loaded = True
81
  elif not uploaded_files:
82
  st.session_state.json_data.clear()
83
  st.session_state.files_loaded = False
84
 
85
- # --- Recursive search for key/value in all files
86
- def search_all_jsons(key, value):
87
- found = []
88
- for file_name, data in st.session_state.json_data.items():
89
- def recursive_search(obj):
90
- if isinstance(obj, dict):
91
- if key in obj and str(obj[key]).lower() == str(value).lower():
92
- found.append({**obj, "__file__": file_name})
93
- for v in obj.values():
94
- recursive_search(v)
95
- elif isinstance(obj, list):
96
- for item in obj:
97
- recursive_search(item)
98
- recursive_search(data)
99
- return found
100
-
101
- # --- Other functions for LLM
102
- def search_json(file_name, key, value):
103
- def recursive_search(obj, key, value, found):
104
  if isinstance(obj, dict):
105
- if key in obj and str(obj[key]).lower() == str(value).lower():
106
- found.append(obj)
107
- for v in obj.values():
108
- recursive_search(v, key, value, found)
 
 
 
 
 
 
 
 
 
 
 
 
109
  elif isinstance(obj, list):
110
- for item in obj:
111
- recursive_search(item, key, value, found)
112
- return found
113
- try:
114
- data = st.session_state.json_data[file_name]
115
- results = recursive_search(data, key, value, [])
116
- return results
117
- except Exception as e:
118
- return {"error": str(e)}
119
-
120
- def list_keys(file_name):
121
- try:
122
- data = st.session_state.json_data[file_name]
123
- if isinstance(data, dict):
124
- return list(data.keys())
125
- elif isinstance(data, list) and data and isinstance(data[0], dict):
126
- return list(data[0].keys())
127
- else:
128
- return []
129
- except Exception as e:
130
- return {"error": str(e)}
131
-
132
- def count_key_occurrences(file_name, key):
133
- try:
134
- data = st.session_state.json_data[file_name]
135
- if isinstance(data, dict):
136
- return 1 if key in data else 0
137
- elif isinstance(data, list):
138
- return sum(1 for item in data if isinstance(item, dict) and key in item)
139
- else:
140
- return 0
141
- except Exception as e:
142
- return {"error": str(e)}
143
-
144
- # --- Function schema
145
- function_schema = [
146
- {
147
- "name": "search_json",
148
- "description": "Find records in the specified JSON file where key matches a given value.",
149
- "parameters": {
150
- "type": "object",
151
- "properties": {
152
- "file_name": {"type": "string", "description": "The uploaded JSON file to search."},
153
- "key": {"type": "string", "description": "The key/field to filter by."},
154
- "value": {"type": "string", "description": "The value to match."}
155
- },
156
- "required": ["file_name", "key", "value"],
157
- },
158
- },
159
- {
160
- "name": "list_keys",
161
- "description": "List all top-level keys in a given JSON file.",
162
- "parameters": {
163
- "type": "object",
164
- "properties": {
165
- "file_name": {"type": "string", "description": "The uploaded JSON file."},
166
- },
167
- "required": ["file_name"],
168
- },
169
- },
170
- {
171
- "name": "count_key_occurrences",
172
- "description": "Count the number of times a given key appears in a JSON file.",
173
- "parameters": {
174
- "type": "object",
175
- "properties": {
176
- "file_name": {"type": "string", "description": "The uploaded JSON file."},
177
- "key": {"type": "string", "description": "The key to count."},
178
- },
179
- "required": ["file_name", "key"],
180
- },
181
- },
182
- {
183
- "name": "search_all_jsons",
184
- "description": "Search all uploaded JSON files recursively for dicts where a key matches a value.",
185
- "parameters": {
186
- "type": "object",
187
- "properties": {
188
- "key": {"type": "string", "description": "The key to search for (e.g. 'gender')"},
189
- "value": {"type": "string", "description": "The value to match (e.g. 'female')"}
190
- },
191
- "required": ["key", "value"]
192
- }
193
- }
194
- ]
195
-
196
- # --- Conversation UI
197
- st.markdown("### Conversation")
198
- for i, msg in enumerate(st.session_state.messages[1:]):
199
  if msg["role"] == "user":
200
  st.markdown(f"<div style='color: #4F8BF9;'><b>User:</b> {msg['content']}</div>", unsafe_allow_html=True)
201
- elif msg["role"] == "assistant":
202
- content = msg.get("content", "")
203
- if content.strip():
204
- st.markdown(f"<div style='color: #1C6E4C;'><b>Agent:</b> {content}</div>", unsafe_allow_html=True)
205
- else:
206
- st.markdown(f"<div style='color: #DC143C;'><b>Agent:</b> [No response generated]</div>", unsafe_allow_html=True)
207
- elif msg["role"] == "function":
208
- try:
209
- result = json.loads(msg["content"])
210
- st.markdown(f"<details><summary><b>Function '{msg['name']}' output:</b></summary><pre>{json.dumps(result, indent=2)}</pre></details>", unsafe_allow_html=True)
211
- except Exception:
212
- st.markdown(f"<b>Function '{msg['name']}' output:</b> {msg['content']}", unsafe_allow_html=True)
213
 
214
- # --- Chat input and OpenAI handling
215
  def send_message():
216
- try:
217
- user_input = st.session_state.temp_input
218
- if user_input and user_input.strip():
219
- st.session_state.messages.append({"role": "user", "content": user_input})
220
- chat_messages = st.session_state.messages
221
- if len(chat_messages) > 10:
222
- chat_messages = [chat_messages[0]] + chat_messages[-9:]
223
- else:
224
- chat_messages = chat_messages.copy()
225
- chat_resp = requests.post(
226
- "https://api.openai.com/v1/chat/completions",
227
- headers=HEADERS,
228
- json={
229
- "model": "gpt-4.1",
230
- "messages": chat_messages,
231
- "functions": function_schema,
232
- "function_call": "auto",
233
- "temperature": 0,
234
- "max_tokens": 1000,
235
- },
236
- timeout=60,
237
- )
238
- chat_resp.raise_for_status()
239
- response_json = chat_resp.json()
240
- msg = response_json["choices"][0]["message"]
241
-
242
- if msg.get("function_call"):
243
- func_name = msg["function_call"]["name"]
244
- args_json = msg["function_call"]["arguments"]
245
- args = json.loads(args_json)
246
-
247
- if func_name == "search_json":
248
- result = search_json(args.get("file_name"), args.get("key"), args.get("value"))
249
- elif func_name == "list_keys":
250
- result = list_keys(args.get("file_name"))
251
- elif func_name == "count_key_occurrences":
252
- result = count_key_occurrences(args.get("file_name"), args.get("key"))
253
- elif func_name == "search_all_jsons":
254
- result = search_all_jsons(args.get("key"), args.get("value"))
255
- else:
256
- result = {"error": f"Unknown function: {func_name}"}
257
-
258
- st.session_state.messages.append({
259
- "role": "function",
260
- "name": func_name,
261
- "content": json.dumps(result),
262
- })
263
- followup_messages = st.session_state.messages
264
- if len(followup_messages) > 12:
265
- followup_messages = [followup_messages[0]] + followup_messages[-11:]
266
- else:
267
- followup_messages = followup_messages.copy()
268
- final_resp = requests.post(
269
- "https://api.openai.com/v1/chat/completions",
270
- headers=HEADERS,
271
- json={
272
- "model": "gpt-4.1",
273
- "messages": followup_messages,
274
- "temperature": 0,
275
- "max_tokens": 1500,
276
- },
277
- timeout=60,
278
- )
279
- final_resp.raise_for_status()
280
- final_json = final_resp.json()
281
- answer = final_json["choices"][0]["message"]["content"]
282
- st.session_state.messages.append({"role": "assistant", "content": answer})
283
- else:
284
- st.session_state.messages.append({"role": "assistant", "content": msg["content"]})
285
  st.session_state.temp_input = ""
286
- except Exception as e:
287
- st.error("Exception: " + str(e))
288
- st.code(traceback.format_exc())
289
 
290
  if st.session_state.json_data:
291
  st.text_input("Your message:", key="temp_input", on_change=send_message)
292
- if st.button("Test: Count females in all JSONs"):
293
- results = search_all_jsons("gender", "female")
294
- st.write(f"Females found: {len(results)}")
295
- st.json(results)
296
  else:
297
  st.info("Please upload at least one JSON file to start chatting.")
 
1
  import streamlit as st
2
  import os
3
  import json
 
4
  import traceback
5
+ from datetime import datetime
6
+
7
+ # ---- CONFIG ----
8
+ COMMON_NAME_KEYS = ["user", "username", "name", "fullName", "firstName", "lastName"]
9
+ LOGIN_KEYS = ["lastLogin", "login", "loggedIn", "lastLoggedIn", "last_login", "last_logged_in"]
10
 
11
  # --- ALWAYS INIT SESSION STATE FIRST (before any widgets)
12
  if "json_data" not in st.session_state:
 
18
  if "files_loaded" not in st.session_state:
19
  st.session_state.files_loaded = False
20
 
21
+ st.set_page_config(page_title="Instant JSON Q&A", layout="wide")
22
+ st.title("Instant JSON-Backed AI Q&A (No More Clarifying Loops)")
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
+ # --- Upload area
25
  st.sidebar.header("Upload Multiple JSON Files")
26
  uploaded_files = st.sidebar.file_uploader(
27
  "Choose one or more JSON files", type="json", accept_multiple_files=True
28
  )
29
 
30
+ # --- Load/clear files as needed
31
  if uploaded_files and not st.session_state.files_loaded:
32
  st.session_state.json_data.clear()
 
33
  for f in uploaded_files:
34
  try:
35
  content = json.load(f)
36
  st.session_state.json_data[f.name] = content
 
 
 
 
 
 
 
37
  st.sidebar.success(f"Loaded: {f.name}")
 
38
  except Exception as e:
39
  st.sidebar.error(f"Error reading {f.name}: {e}")
40
+ st.session_state.messages = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  st.session_state.files_loaded = True
42
  elif not uploaded_files:
43
  st.session_state.json_data.clear()
44
  st.session_state.files_loaded = False
45
 
46
+ # --- UNIVERSAL SEARCH FUNCTION ----
47
+ def recursive_find_user(obj, target_name):
48
+ """
49
+ Recursively search for dicts containing a value matching target_name under any COMMON_NAME_KEYS.
50
+ Returns list of matches: dicts with the matching name, file name, and all possible login info found.
51
+ """
52
+ matches = []
53
+ def _search(obj, path, file_name):
 
 
 
 
 
 
 
 
 
 
 
54
  if isinstance(obj, dict):
55
+ for k, v in obj.items():
56
+ # Check if any "name" key matches (case-insensitive substring match)
57
+ if k in COMMON_NAME_KEYS and target_name.lower() in str(v).lower():
58
+ login_info = {}
59
+ # Find login info at this level
60
+ for lk in LOGIN_KEYS:
61
+ if lk in obj:
62
+ login_info[lk] = obj[lk]
63
+ matches.append({
64
+ "match_path": path + [k],
65
+ "matched_name": v,
66
+ "record": obj,
67
+ "file": file_name,
68
+ "login_info": login_info
69
+ })
70
+ _search(v, path + [k], file_name)
71
  elif isinstance(obj, list):
72
+ for idx, item in enumerate(obj):
73
+ _search(item, path + [f"[{idx}]"], file_name)
74
+ for file_name, data in st.session_state.json_data.items():
75
+ _search(data, [], file_name)
76
+ return matches
77
+
78
+ # --- MAIN QUERY LOGIC ---
79
+ def handle_user_query(query):
80
+ # 1. If question looks like "last login for <name>", extract name and search
81
+ import re
82
+ # Covers "last login for", "when did X last login", "when was X last seen", etc.
83
+ patterns = [
84
+ r"(?:last\s*login.*?for|when\s+did)\s+(.*?)\?*$", # e.g. last login for Bob, when did Bob last login
85
+ r"when\s+was\s+(.*?)\s+last\s+(?:login|logged\s*in)", # when was Bob last logged in
86
+ r"last\s*login\s*of\s+(.*?)\?*$",
87
+ r"(?:info|details|record) for\s+(.*?)\?*$"
88
+ ]
89
+ found_name = None
90
+ for pat in patterns:
91
+ m = re.search(pat, query, re.IGNORECASE)
92
+ if m:
93
+ found_name = m.group(1).strip()
94
+ break
95
+ if not found_name:
96
+ # Fallback: just look for the first capitalized word (e.g. Bob, Bob the Builder)
97
+ m = re.search(r"([A-Z][a-z]+(?: [A-Z][a-z]+)*)", query)
98
+ if m:
99
+ found_name = m.group(1).strip()
100
+ if found_name:
101
+ results = recursive_find_user(st.session_state.json_data, found_name)
102
+ if not results:
103
+ return f"No records found for '{found_name}' in any file."
104
+ # Report each match and all login fields found
105
+ answers = []
106
+ for res in results:
107
+ login = ", ".join([f"{k}: {v}" for k, v in res["login_info"].items()]) if res["login_info"] else "No login info found"
108
+ answers.append(
109
+ f"**{res['matched_name']}** (in file `{res['file']}`) — {login}"
110
+ )
111
+ return "\n\n".join(answers)
112
+ else:
113
+ # For all other queries, just report "Sorry, direct name lookups only. (Add more query logic if you want!)"
114
+ return "Sorry, I can only answer direct user info queries (e.g., 'When did Bob the Builder last login?'). If you want more logic, let me know!"
115
+
116
+ # --- CHAT UI (NO LLM ROUNDTRIP!)
117
+ st.markdown("### Ask about any user directly (e.g. 'When did Bob the Builder last login?')")
118
+ for msg in st.session_state.messages:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  if msg["role"] == "user":
120
  st.markdown(f"<div style='color: #4F8BF9;'><b>User:</b> {msg['content']}</div>", unsafe_allow_html=True)
121
+ else:
122
+ st.markdown(f"<div style='color: #1C6E4C;'><b>Agent:</b> {msg['content']}</div>", unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
123
 
 
124
  def send_message():
125
+ user_input = st.session_state.temp_input
126
+ if user_input.strip():
127
+ st.session_state.messages.append({"role": "user", "content": user_input})
128
+ answer = handle_user_query(user_input)
129
+ st.session_state.messages.append({"role": "assistant", "content": answer})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  st.session_state.temp_input = ""
 
 
 
131
 
132
  if st.session_state.json_data:
133
  st.text_input("Your message:", key="temp_input", on_change=send_message)
 
 
 
 
134
  else:
135
  st.info("Please upload at least one JSON file to start chatting.")