Liss, Alex (NYC-HUG) commited on
Commit
f3a3878
·
1 Parent(s): 20ecd50

WIP implementation of team search, have improved component display of game and player search too

Browse files
components/team_story_component.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Gradio component for displaying Team Story/News search results.
3
+ """
4
+
5
+ import gradio as gr
6
+
7
+ def create_team_story_component(team_story_data):
8
+ """
9
+ Creates a Gradio HTML component to display formatted team news articles.
10
+
11
+ Args:
12
+ team_story_data (list): A list of dictionaries, where each dictionary
13
+ represents an article and contains keys like
14
+ 'summary', 'link_to_article', and 'topic'.
15
+
16
+ Returns:
17
+ gr.HTML: A Gradio HTML component containing the formatted news stories.
18
+ Returns None if the input data is empty or invalid.
19
+ """
20
+ if not team_story_data or not isinstance(team_story_data, list):
21
+ return None # Return None if no data or invalid data
22
+
23
+ html_content = """<div style='padding: 15px; border: 1px solid #e0e0e0; border-radius: 5px; margin-top: 10px;'>
24
+ <h3 style='margin-top: 0; margin-bottom: 10px;'>Recent Team News</h3>"""
25
+
26
+ for story in team_story_data:
27
+ if isinstance(story, dict):
28
+ summary = story.get('summary', 'No summary available.')
29
+ link = story.get('link_to_article', '#')
30
+ topic = story.get('topic', 'General')
31
+
32
+ # Sanitize link to prevent basic injection issues
33
+ safe_link = link if link.startswith(('http://', 'https://', '#')) else '#'
34
+
35
+ # Escape basic HTML characters in text fields
36
+ def escape_html(text):
37
+ return text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
38
+
39
+ safe_summary = escape_html(summary)
40
+ safe_topic = escape_html(topic)
41
+
42
+ html_content += f"""<div style='margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #eee;'>
43
+ <p style='font-size: 0.9em; color: #555; margin-bottom: 5px;'><strong>Topic:</strong> {safe_topic}</p>
44
+ <p style='margin-bottom: 8px;'>{safe_summary}</p>
45
+ <a href='{safe_link}' target='_blank' style='font-size: 0.9em;'>Read Full Article</a>
46
+ </div>"""
47
+ else:
48
+ print(f"Warning: Skipping invalid item in team_story_data: {story}")
49
+
50
+ # Remove the last border-bottom if content was added
51
+ if len(team_story_data) > 0:
52
+ last_border_pos = html_content.rfind("border-bottom: 1px solid #eee;")
53
+ if last_border_pos != -1:
54
+ html_content = html_content[:last_border_pos] + html_content[last_border_pos:].replace("border-bottom: 1px solid #eee;", "")
55
+
56
+ html_content += "</div>"
57
+
58
+ # Return None if only the initial header was created (e.g., all items were invalid)
59
+ if html_content.strip() == """<div style='padding: 15px; border: 1px solid #e0e0e0; border-radius: 5px; margin-top: 10px;'>
60
+ <h3 style='margin-top: 0; margin-bottom: 10px;'>Recent Team News</h3></div>""".strip():
61
+ return None
62
+
63
+ return gr.HTML(html_content)
gradio_agent.py CHANGED
@@ -20,8 +20,9 @@ from gradio_utils import get_session_id
20
  # Import tools
21
  from tools.cypher import cypher_qa_wrapper
22
  from tools.vector import get_game_summary
23
- from tools.game_recap import game_recap_qa
24
- from tools.player_search import player_search_qa # Import the new player search tool
 
25
 
26
  # Create a basic chat chain for general football discussion
27
  from langchain_core.prompts import ChatPromptTemplate
@@ -91,6 +92,13 @@ Examples: "Tell me about Brock Purdy", "Who is player number 97?", "Show me Nick
91
  Returns text summary and potentially visual card data.""",
92
  func=player_search_qa
93
  ),
 
 
 
 
 
 
 
94
  Tool.from_function(
95
  name="Game Recap",
96
  description="""Use SPECIFICALLY for detailed game recaps or when users want to see visual information about a particular game identified by opponent or date.
 
20
  # Import tools
21
  from tools.cypher import cypher_qa_wrapper
22
  from tools.vector import get_game_summary
23
+ from tools.game_recap import game_recap_qa, get_last_game_data
24
+ from tools.player_search import player_search_qa, get_last_player_data
25
+ from tools.team_story import team_story_qa, get_last_team_story_data
26
 
27
  # Create a basic chat chain for general football discussion
28
  from langchain_core.prompts import ChatPromptTemplate
 
92
  Returns text summary and potentially visual card data.""",
93
  func=player_search_qa
94
  ),
95
+ Tool.from_function(
96
+ name="Team News Search",
97
+ description="""Use for questions about recent 49ers news, articles, summaries, or specific topics like 'draft' or 'roster moves'.
98
+ Examples: 'What's the latest team news?', 'Summarize recent articles about the draft', 'Any news about the offensive line?'
99
+ Returns text summary and potentially structured article data.""",
100
+ func=team_story_qa
101
+ ),
102
  Tool.from_function(
103
  name="Game Recap",
104
  description="""Use SPECIFICALLY for detailed game recaps or when users want to see visual information about a particular game identified by opponent or date.
gradio_app.py CHANGED
@@ -11,6 +11,7 @@ from gradio_llm import llm
11
  import gradio_utils
12
  from components.game_recap_component import create_game_recap_component
13
  from components.player_card_component import create_player_card_component
 
14
 
15
  # Import the Gradio-compatible agent instead of the original agent
16
  import gradio_agent
@@ -19,6 +20,7 @@ from gradio_agent import generate_response
19
  # Import cache getter functions
20
  from tools.game_recap import get_last_game_data
21
  from tools.player_search import get_last_player_data
 
22
 
23
  # Define CSS directly
24
  css = """
@@ -286,20 +288,18 @@ with gr.Blocks(title="49ers FanAI Hub", theme=gr.themes.Soft(), css=css) as demo
286
  gr.Markdown("# 🏈 49ers FanAI Hub")
287
 
288
  # --- Component Display Area --- #
289
- # Debug Textbox (Temporary)
290
- debug_textbox = gr.Textbox(label="Debug Player Data", visible=True, interactive=False)
291
- # Player card display (initially hidden)
292
- player_card_display = gr.HTML(visible=False)
293
- # Game recap display (initially hidden)
294
- game_recap_display = gr.HTML(visible=False)
295
-
296
- # Chat interface
297
  chatbot = gr.Chatbot(
298
  # value=state.get_chat_history(), # Let Gradio manage history display directly
299
  height=500,
300
  show_label=False,
301
  elem_id="chatbot",
302
- # type="messages", # Default type often works better
303
  render_markdown=True
304
  )
305
 
@@ -314,76 +314,92 @@ with gr.Blocks(title="49ers FanAI Hub", theme=gr.themes.Soft(), css=css) as demo
314
 
315
  # Define a combined function for user input and bot response
316
  async def process_and_respond(message, history):
317
- print(f"History IN: {history}")
318
- # Initialize if first interaction
319
- if not state.initialized:
320
- welcome_msg = await initialize_chat()
321
- history = [(None, welcome_msg)] # Start history with welcome message
322
- state.initialized = True
323
- # Return immediately after initialization to show welcome message
324
- # No component updates needed yet
325
- return "", history, gr.update(visible=False), gr.update(visible=False), gr.update(value="")
326
-
327
- # Append user message for processing & display
328
- history.append((message, None))
329
-
330
- # Process the message to get bot's text response
331
- response_text = await process_message(message)
332
-
333
- # Update last message in history with bot response
334
- history[-1] = (message, response_text)
335
- print(f"History OUT: {history}")
336
-
337
- # --- Check Caches and Update Components --- #
338
- # player_card_html = "" # No longer creating HTML directly here for player
339
- player_card_visible = False
340
- game_recap_html = ""
341
- game_recap_visible = False
342
- debug_text_update = gr.update(value="") # Default debug text update
343
-
344
- # Check for Player Data first
345
- player_data = get_last_player_data() # Check player cache
346
  if player_data:
347
- print("Player data found in cache, updating debug textbox...")
348
- # player_card_html = create_player_card_component(player_data) # Temporarily disable HTML generation
349
- player_card_visible = False # Keep HTML hidden for now
350
- debug_text_update = gr.update(value=str(player_data)) # Update debug textbox instead
351
- else:
352
- # If no player data, check for Game Data
353
- game_data = get_last_game_data() # Check game cache
354
- if game_data:
355
- print("Game data found in cache, creating recap...")
356
- game_recap_html = create_game_recap_component(game_data)
357
- game_recap_visible = True
358
- # No player data found, ensure debug text is cleared
359
- debug_text_update = gr.update(value="")
360
-
361
- # Return updates for all relevant components
362
- # Note: player_card_display update is temporarily disabled (always hidden)
363
- return (
364
- "",
365
- history,
366
- debug_text_update, # Update for the debug textbox
367
- gr.update(value="", visible=False), # Keep player HTML hidden
368
- gr.update(value=game_recap_html, visible=game_recap_visible) # Update game recap as before
369
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
370
 
371
  # Set up event handlers with the combined function
372
  # Ensure outputs list matches the return values of process_and_respond
373
- # Added debug_textbox to the outputs list
374
- outputs_list = [msg, chatbot, debug_textbox, player_card_display, game_recap_display]
375
  msg.submit(process_and_respond, [msg, chatbot], outputs_list)
376
  submit_btn.click(process_and_respond, [msg, chatbot], outputs_list)
377
 
378
  # Add a clear button
379
  clear_btn = gr.Button("Clear Conversation")
380
 
381
- # Clear function - now needs to clear/hide components including debug box
382
  def clear_chat():
383
- return [], gr.update(value=""), gr.update(value="", visible=False), gr.update(value="", visible=False)
 
384
 
385
- # Update clear outputs
386
- clear_btn.click(clear_chat, None, [chatbot, debug_textbox, player_card_display, game_recap_display])
387
 
388
  # Launch the app
389
  if __name__ == "__main__":
 
11
  import gradio_utils
12
  from components.game_recap_component import create_game_recap_component
13
  from components.player_card_component import create_player_card_component
14
+ from components.team_story_component import create_team_story_component
15
 
16
  # Import the Gradio-compatible agent instead of the original agent
17
  import gradio_agent
 
20
  # Import cache getter functions
21
  from tools.game_recap import get_last_game_data
22
  from tools.player_search import get_last_player_data
23
+ from tools.team_story import get_last_team_story_data
24
 
25
  # Define CSS directly
26
  css = """
 
288
  gr.Markdown("# 🏈 49ers FanAI Hub")
289
 
290
  # --- Component Display Area --- #
291
+ # REMOVED Unused/Redundant Component Placeholders:
292
+ # debug_textbox = gr.Textbox(label="Debug Player Data", visible=True, interactive=False)
293
+ # player_card_display = gr.HTML(visible=False)
294
+ # game_recap_display = gr.HTML(visible=False)
295
+
296
+ # Chat interface - Components will be added directly here
 
 
297
  chatbot = gr.Chatbot(
298
  # value=state.get_chat_history(), # Let Gradio manage history display directly
299
  height=500,
300
  show_label=False,
301
  elem_id="chatbot",
302
+ # type="messages", # Using default tuple format as components are added
303
  render_markdown=True
304
  )
305
 
 
314
 
315
  # Define a combined function for user input and bot response
316
  async def process_and_respond(message, history):
317
+ """Process user input, get agent response, check for components, and update history."""
318
+
319
+ print(f"process_and_respond: Received message: {message}")
320
+ # history.append((message, None)) # Add user message placeholder
321
+ # yield "", history # Show user message immediately
322
+
323
+ # Call the agent to get the response (text output + potentially populates cached data)
324
+ agent_response = generate_response(message, state.session_id)
325
+ text_output = agent_response.get("output", "Sorry, something went wrong.")
326
+ metadata = agent_response.get("metadata", {})
327
+ tools_used = metadata.get("tools_used", ["None"])
328
+
329
+ print(f"process_and_respond: Agent text output: {text_output}")
330
+ print(f"process_and_respond: Tools used: {tools_used}")
331
+
332
+ # Initialize response list with the text output
333
+ response_list = [(message, text_output)]
334
+
335
+ # Check for specific component data based on tools used or cached data
336
+ # Important: Call the getter functions *after* generate_response has run
337
+
338
+ # Check for Player Card
339
+ player_data = get_last_player_data()
 
 
 
 
 
 
340
  if player_data:
341
+ print(f"process_and_respond: Found player data: {player_data}")
342
+ player_card_component = create_player_card_component(player_data)
343
+ if player_card_component:
344
+ response_list.append((None, player_card_component))
345
+ print("process_and_respond: Added player card component.")
346
+ else:
347
+ print("process_and_respond: Player data found but component creation failed.")
348
+
349
+ # Check for Game Recap
350
+ game_data = get_last_game_data()
351
+ if game_data:
352
+ print(f"process_and_respond: Found game data: {game_data}")
353
+ game_recap_comp = create_game_recap_component(game_data)
354
+ if game_recap_comp:
355
+ response_list.append((None, game_recap_comp))
356
+ print("process_and_respond: Added game recap component.")
357
+ else:
358
+ print("process_and_respond: Game data found but component creation failed.")
359
+
360
+ # Check for Team Story --- NEW ---
361
+ team_story_data = get_last_team_story_data()
362
+ if team_story_data:
363
+ print(f"process_and_respond: Found team story data: {team_story_data}")
364
+ team_story_comp = create_team_story_component(team_story_data)
365
+ if team_story_comp:
366
+ response_list.append((None, team_story_comp))
367
+ print("process_and_respond: Added team story component.")
368
+ else:
369
+ print("process_and_respond: Team story data found but component creation failed.")
370
+
371
+ # Update history with all parts of the response (text + components)
372
+ # Gradio's Chatbot handles lists of (user, assistant) tuples,
373
+ # where assistant can be text or a Gradio component.
374
+ # We replace the last entry (user, None) with the actual response items.
375
+
376
+ # Gradio manages history display; we just return the latest exchange.
377
+ # The actual history state is managed elsewhere (e.g., Zep, Neo4j history)
378
+
379
+ # Return the combined response list to update the chatbot UI
380
+ # The first element is user message + assistant text response
381
+ # Subsequent elements are None + UI component
382
+ print(f"process_and_respond: Final response list for UI: {response_list}")
383
+ # Return values suitable for outputs: [msg, chatbot]
384
+ return "", response_list # Return empty string for msg, list for chatbot
385
 
386
  # Set up event handlers with the combined function
387
  # Ensure outputs list matches the return values of process_and_respond
388
+ # REMOVED redundant components from outputs_list
389
+ outputs_list = [msg, chatbot]
390
  msg.submit(process_and_respond, [msg, chatbot], outputs_list)
391
  submit_btn.click(process_and_respond, [msg, chatbot], outputs_list)
392
 
393
  # Add a clear button
394
  clear_btn = gr.Button("Clear Conversation")
395
 
396
+ # Clear function - now only needs to clear msg and chatbot
397
  def clear_chat():
398
+ # Return empty values for msg and chatbot
399
+ return "", []
400
 
401
+ # Update clear outputs - only need msg and chatbot
402
+ clear_btn.click(clear_chat, None, [msg, chatbot])
403
 
404
  # Launch the app
405
  if __name__ == "__main__":
tools/team_story.py ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Tool for querying Neo4j about recent team news stories.
3
+ """
4
+
5
+ import os
6
+ import sys
7
+ from dotenv import load_dotenv
8
+ from langchain_core.prompts import PromptTemplate
9
+ from langchain_openai import ChatOpenAI
10
+ from langchain_community.chains.graph_qa.cypher import GraphCypherQAChain
11
+
12
+ # Adjust path to import graph object and LLM from the parent directory
13
+ parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
14
+ if parent_dir not in sys.path:
15
+ sys.path.append(parent_dir)
16
+
17
+ try:
18
+ from gradio_graph import graph # Import the configured graph instance
19
+ from gradio_llm import llm # Import the configured LLM instance
20
+ except ImportError as e:
21
+ print(f"Error importing graph or llm: {e}")
22
+ print("Please ensure gradio_graph.py and gradio_llm.py exist and are configured correctly.")
23
+ sys.exit(1)
24
+
25
+ # Load environment variables if needed (though graph/llm should be configured)
26
+ load_dotenv()
27
+
28
+ # Define the prompt for translating NL query to Cypher for Team Story
29
+ CYPHER_TEAM_STORY_GENERATION_TEMPLATE = """
30
+ Task: Generate Cypher query to query a graph database.
31
+ Instructions:
32
+ Use only the provided relationship types and properties in the schema.
33
+ Do not use any other relationship types or properties that are not provided.
34
+ Schema:
35
+ {schema}
36
+ Note: Do not include any explanations or apologies in your responses.
37
+ Do not respond to any questions that might ask anything else than for you to construct a Cypher query.
38
+ Do not include any text except the generated Cypher query.
39
+
40
+ The question is:
41
+ {query}
42
+
43
+ Cypher Query:
44
+ """
45
+
46
+ CYPHER_TEAM_STORY_GENERATION_PROMPT = PromptTemplate(
47
+ input_variables=["schema", "query"], template=CYPHER_TEAM_STORY_GENERATION_TEMPLATE
48
+ )
49
+
50
+ # Placeholder for structured data caching (similar to player_search/game_recap)
51
+ LAST_TEAM_STORY_DATA = []
52
+
53
+ def get_last_team_story_data():
54
+ """Returns the structured data from the last team story query."""
55
+ return LAST_TEAM_STORY_DATA
56
+
57
+ def team_story_qa(query: str) -> dict:
58
+ """
59
+ Queries the Neo4j database for team news stories based on the user query.
60
+
61
+ Args:
62
+ query: The natural language query from the user.
63
+
64
+ Returns:
65
+ A dictionary containing the 'output' text and potentially structured 'team_story_data'.
66
+ """
67
+ global LAST_TEAM_STORY_DATA
68
+ LAST_TEAM_STORY_DATA = [] # Clear previous results
69
+
70
+ print(f"--- Running Team Story QA for query: {query} ---")
71
+
72
+ try:
73
+ # Initialize the QA chain for Team Story
74
+ team_story_chain = GraphCypherQAChain.from_llm(
75
+ llm=llm, # Use the pre-configured LLM
76
+ graph=graph, # Use the pre-configured graph connection
77
+ cypher_prompt=CYPHER_TEAM_STORY_GENERATION_PROMPT,
78
+ verbose=True, # Set to True for debugging Cypher generation
79
+ return_intermediate_steps=True, # Useful for debugging
80
+ return_direct=False # Return the final answer directly? Set to False for now
81
+ )
82
+
83
+ # Use the GraphCypherQAChain to get results
84
+ # The chain handles NL -> Cypher -> Execution -> LLM response generation
85
+ result = team_story_chain.invoke({"query": query})
86
+
87
+ print(f"Raw result from team_story_chain: {result}")
88
+
89
+ # Extract relevant info (summaries, links) from the result['intermediate_steps']
90
+ # This structure depends on GraphCypherQAChain's output
91
+ # Typically intermediate_steps contains context (list of dicts)
92
+ structured_results = []
93
+ if 'intermediate_steps' in result and isinstance(result['intermediate_steps'], list):
94
+ for step in result['intermediate_steps']:
95
+ if 'context' in step and isinstance(step['context'], list):
96
+ for item in step['context']:
97
+ # Assuming item is a dict representing a :Team_Story node
98
+ if isinstance(item, dict):
99
+ story_data = {
100
+ 'summary': item.get('s.summary', 'Summary not available'),
101
+ 'link_to_article': item.get('s.link_to_article', '#'),
102
+ 'topic': item.get('s.topic', 'Topic not available')
103
+ }
104
+ structured_results.append(story_data)
105
+
106
+ LAST_TEAM_STORY_DATA = structured_results
107
+
108
+ # Format the text output
109
+ if not structured_results:
110
+ output_text = result.get('result', "I couldn't find any specific news articles matching your query, but I can try a broader search.")
111
+ else:
112
+ # Simple text formatting for now
113
+ output_text = result.get('result', "Here's what I found:") + "\n\n"
114
+ for i, story in enumerate(structured_results[:3]): # Show top 3
115
+ output_text += f"{i+1}. {story['summary']}\n[Link: {story['link_to_article']}]\n\n"
116
+ if len(structured_results) > 3:
117
+ output_text += f"... found {len(structured_results)} relevant articles in total."
118
+
119
+ except Exception as e:
120
+ print(f"Error during team_story_qa: {e}")
121
+ output_text = "Sorry, I encountered an error trying to find team news."
122
+ LAST_TEAM_STORY_DATA = []
123
+
124
+ print(f"--- Team Story QA output: {output_text} ---")
125
+ print(f"--- Team Story QA structured data: {LAST_TEAM_STORY_DATA} ---")
126
+
127
+ # Return both text output and structured data (though structured data isn't used by agent yet)
128
+ return {"output": output_text, "team_story_data": LAST_TEAM_STORY_DATA}
129
+
130
+ # Example usage (for testing)
131
+ if __name__ == '__main__':
132
+ test_query = "What is the latest news about the 49ers draft?"
133
+ print(f"Testing team_story_qa with query: {test_query}")
134
+ response = team_story_qa(test_query)
135
+ print("\nResponse:")
136
+ print(response.get("output"))
137
+ print("\nStructured Data:")
138
+ print(response.get("team_story_data"))
139
+
140
+ test_query_2 = "Any updates on the roster?"
141
+ print(f"\nTesting team_story_qa with query: {test_query_2}")
142
+ response_2 = team_story_qa(test_query_2)
143
+ print("\nResponse:")
144
+ print(response_2.get("output"))
145
+ print("\nStructured Data:")
146
+ print(response_2.get("team_story_data"))