diff --git a/cached_data.py b/cached_data.py index 0d08d8b..a6910f0 100644 --- a/cached_data.py +++ b/cached_data.py @@ -1,29 +1,107 @@ import cachetools.func import pandas as pd - +import json from motherduck import con import polars as pl - +from langchain.tools import tool +import numpy as np +import opr3 CACHE_SECONDS = 600 +def convert_ndarrays(obj): + if isinstance(obj, np.ndarray): + return obj.tolist() + return obj + +@tool +def get_bot_matches(event_key:str) -> str: + """ + Given an FRC event key like '2025schar', returns all of the matches played + + the keys in the response are of the format 'scoring_category_z', so you can take off + the _z suffix when matching data from a user query. + + Accepts: + - event_key: string like "2025schar" + + Returns: + - JSON string listing all match data, including which teams played ( red1, red2, red3, and blue1, blue2,blue3) + as well as the scores for both teams, the match time, and all of the bonus achievements and scoring in the match + """ + m = get_matches_for_event(event_key) + return json.dumps(m.to_dict(orient='records'),indent=2) + def get_matches_for_event(event_key:str) -> pd.DataFrame: all_matches = get_matches() return all_matches [ all_matches['event_key'] == event_key].sort_values(by=['time'], ascending=[True]) +@tool +def get_team_zscores(event_key:str) -> str: + """ + Given an FRC event key like '2025schar', returns the z scores for every robot + in all blue alliance performance categories. + see the statistics term z-score. + + the keys in the response are of the format 'scoring_category_z', so you can take off + the _z suffix when matching data from a user query. + + Accepts: + - event_key: string like "2025schar" + + Returns: + - JSON string listing each scoring area, with an _z after it, and then for each of those, + a dict of z scores for each team within that scoring category + """ + df = opr3.get_ccm_data_for_event(event_key) + df = opr3.select_z_score_columns(df, ['team_id']) + + df.reset_index(drop=True, inplace=True) + df = df.set_index('team_id') + #df = df.T + df = df.sort_index() + d = df.to_dict() + return json.dumps(d,indent=2) + # Example controller to cache queries # this will only run the query if it needs cache refresh +#@tool @cachetools.func.ttl_cache(maxsize=128, ttl=CACHE_SECONDS) def get_matches() -> pl.DataFrame: + """Gets all of the matches available in the blue alliance""" return con.sql("select * from tba.matches").df(); + @cachetools.func.ttl_cache(maxsize=128, ttl=CACHE_SECONDS) def get_rankings() -> pl.DataFrame: + "Gives rankings for all robots at all events" return con.sql("select * from tba.event_rankings").df(); -@cachetools.func.ttl_cache(maxsize=128, ttl=CACHE_SECONDS) +@tool +def get_defense_bot(event_key: str) -> str: + """ + Given an FRC event key like '2025schar', returns defense bot data including + team number, OPR, drive type, and other stats in JSON format. + + Accepts: + - event_key: string like "2025schar" + + Returns: + - JSON string listing team number, pit data, OPR, drive type, CCWM, and size. + """ + df = get_defense() # returns a polars or pandas DataFrame + + df_clean = df.applymap(convert_ndarrays) + records = df_clean.to_dict(orient="records") + + s = json.dumps(records, indent=2) + print(s) + return s + +#@cachetools.func.ttl_cache(maxsize=128, ttl=CACHE_SECONDS) def get_defense() -> pl.DataFrame: + "gives a data summary for all robots, with data about how well they might play defense. " return con.sql(""" select pit.team_number, pit.drive_type, GREATEST(height,width) as max_size, t.all_tags, o.oprs as opr, o.dprs as dpr, o.ccwms as ccwm, @@ -51,8 +129,10 @@ def get_defense() -> pl.DataFrame: order by drive_rank asc, max_size desc, dpr desc; """).df() +#@tool @cachetools.func.ttl_cache(maxsize=128, ttl=CACHE_SECONDS) def get_team_list(event_key:str) -> list: + "gets a data frame of all the teams available, based on input of an event key" df = con.sql(f""" select red1, red2, red3, blue1, blue2, blue3 from tba.matches @@ -94,8 +174,18 @@ def _get_tba_oprs_and_ranks() -> pd.DataFrame: """).df() return tba_ranks - +@tool def get_tba_oprs_and_ranks_for_event(event_key:str) -> pd.DataFrame: + """ + Given an FRC event key like '2025schar', returns rankings for all bots at the event. + Accepts: + - event_key: string like "2025schar" + + Returns: + - JSON string listing team_number,rank, + avg_rp, opr, wins, losses, ties, total_rp, avg_win_rp, avg_auto_rp avg_coral_rp, + avg_barge_rp, dpr, ccwm + """ r = _get_tba_oprs_and_ranks() r = r[ r['event_key'] == event_key] return r @@ -143,6 +233,7 @@ def _get_robot_specific_value(row, team_number: int, prefix: str, index: i )-> l d.extend(_get_robot_specific_value(row, t, 'blue', 3)) return pd.DataFrame(d) +#@tool @cachetools.func.ttl_cache(maxsize=128, ttl=CACHE_SECONDS) def get_ranking_point_summary_for_event(event_key:str) -> pd.DataFrame: """ diff --git a/pages/08_heatmap.py b/pages/08_heatmap.py index b67be12..6fb67b5 100644 --- a/pages/08_heatmap.py +++ b/pages/08_heatmap.py @@ -1,6 +1,7 @@ import streamlit as st import opr3 from pages_util.event_selector import event_selector +from cached_data import get_team_zscores st.set_page_config(layout="wide") @@ -46,5 +47,5 @@ def style_dataframe(df): st.write(styled_df.to_html(), unsafe_allow_html=True) - +st.write ( get_team_zscores(selected_event)) diff --git a/pages/22_empty_pit_data.py b/pages/22_empty_pit_data.py index 0cc2de0..8507769 100644 --- a/pages/22_empty_pit_data.py +++ b/pages/22_empty_pit_data.py @@ -18,8 +18,14 @@ st.subheader("Not Updated Recently:") all_data = con.sql(""" - select team_number, author, notes,created_at from scouting.pit - where created_at < NOW() - INTERVAL '7' DAY + SELECT team_number, created_at +FROM scouting.pit +WHERE created_at < NOW() - INTERVAL '3 days' +and team_number in ( + 281, 342, 343, 1051, 1102, 1287, 1319, 1758, 2815, 3489, 3490, 4451, 4533, 4864, 5130, 6167, 6366, 7085, 8137, 8575, 9315, 9571, 10231, 10367, 10388, 10591, 10599, 10619 +) """ ).df() st.dataframe(all_data) + + diff --git a/pages/23_defense_picker.py b/pages/23_defense_picker.py index 70b92ec..df503e5 100644 --- a/pages/23_defense_picker.py +++ b/pages/23_defense_picker.py @@ -3,6 +3,12 @@ from cached_data import get_defense import duckdb from motherduck import con +import numpy as np + +def convert_ndarrays(obj): + if isinstance(obj, np.ndarray): + return obj.tolist() + return obj # Event Selector selected_event = event_selector() @@ -42,6 +48,10 @@ st.write("### Visible Teams (Filtered)") st.dataframe(visible_data) +#df_clean = visible_data.applymap(convert_ndarrays) +#records = df_clean.to_dict(orient="records") +#st.write(records) + # Step 6: Allow users to "Re-add All Teams" at the bottom if st.button("Re-add All Teams"): st.session_state.hidden_teams.clear() # Clear hidden teams, restoring all teams diff --git a/pages/24_chat.py b/pages/24_chat.py new file mode 100644 index 0000000..caec3bb --- /dev/null +++ b/pages/24_chat.py @@ -0,0 +1,55 @@ +# main.py +import streamlit as st +from langchain.chat_models import ChatOpenAI +from langchain.agents import create_openai_functions_agent, AgentExecutor +from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder +from cached_data import get_defense_bot,get_tba_oprs_and_ranks_for_event,get_team_zscores,get_bot_matches # your @tool-decorated function +import os + +os.environ["OPENAI_API_KEY"] = st.secrets['openai']["OPEN_API_KEY"] + +# main.py + +#def build_agent(): +# llm = ChatOpenAI(model="gpt-4o", temperature=0) +# tools = [get_defense_bot] +# agent = create_openai_functions_agent(llm=llm, tools=tools) +# return AgentExecutor(agent=agent, tools=tools, verbose=True) + +def build_agent(): + llm = ChatOpenAI(model="gpt-4.1-mini", temperature=0) + tools = [get_defense_bot,get_tba_oprs_and_ranks_for_event,get_team_zscores,get_bot_matches] + prompt = ChatPromptTemplate.from_messages([ + ("system", "You are a helpful assistant that can answer questions about FRC robots."), + MessagesPlaceholder(variable_name="messages"), + ("human", "{input}"), + ("ai", "{agent_scratchpad}") + ]) + + agent = create_openai_functions_agent(llm=llm, tools=tools,prompt=prompt) + return AgentExecutor(agent=agent, tools=tools, verbose=True) + +st.set_page_config(page_title="FRC Defense Chatbot", layout="wide") +st.title("🤖 FRC Defense Assistant") + +if "agent_exec" not in st.session_state: + st.session_state.agent_exec = build_agent() +if "messages" not in st.session_state: + st.session_state.messages = [] + +for msg in st.session_state.messages: + with st.chat_message(msg["role"]): + st.markdown(msg["content"]) + +if user_question := st.chat_input("Ask about defense performance..."): + st.session_state.messages.append({"role": "user", "content": user_question}) + with st.chat_message("user"): + st.markdown(user_question) + with st.chat_message("assistant"): + with st.spinner("Thinking..."): + result = st.session_state.agent_exec.invoke({ + "messages": st.session_state.messages, + "input": user_question}) + output = result.get("output") or str(result) + st.markdown(output) + st.session_state.messages.append({"role": "assistant", "content": output}) diff --git a/requirements.txt b/requirements.txt index edd4f6b..9883a68 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,4 +20,7 @@ scipy pygwalker schedule duckdb==1.1.3 -typer==0.4.2 \ No newline at end of file +typer==0.4.2 +langchain +langchain_community +openai \ No newline at end of file