Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 124 additions & 3 deletions cached_data.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,138 @@
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();

@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,
case pit.drive_type
when 'Swerve' then 1
when 'Tank' then 2
when 'Mecanum' then 3
else 999
end as drive_rank
from scouting.pit

INNER JOIN (
select team_number, list(tag) as all_tags
from scouting.tags
where 'Defense' in (select tag from scouting.tags where team_number = pit.team_number)
group by team_number
) as t
on ( t.team_number = pit.team_number)

INNER JOIN tba.oprs as o
on ( t.team_number = o.team_number and pit.team_number = o.team_number )

where o.event_key = '2025schar'
group by pit.team_number, pit.drive_type, pit.height, pit.width, t.all_tags, o.oprs, o.dprs, o.ccwms, drive_rank
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
Expand Down Expand Up @@ -65,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
Expand Down Expand Up @@ -114,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:
"""
Expand Down Expand Up @@ -193,6 +313,7 @@ def clear_caches():
get_ranking_point_summary_for_event.cache_clear()
_get_tba_oprs_and_ranks.cache_clear()
get_matches.cache_clear()
get_defense.cache_clear()
get_rankings.cache_clear()
get_team_list.cache_clear()
get_events.cache_clear()
4 changes: 4 additions & 0 deletions debug.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[0404/094027.395:ERROR:crashpad_client_win.cc(811)] not connected
[0404/094134.760:ERROR:crashpad_client_win.cc(811)] not connected
[0404/094913.782:ERROR:crashpad_client_win.cc(811)] not connected
[0404/094920.367:ERROR:crashpad_client_win.cc(811)] not connected
3 changes: 2 additions & 1 deletion pages/08_heatmap.py
Original file line number Diff line number Diff line change
@@ -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")


Expand Down Expand Up @@ -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))

57 changes: 57 additions & 0 deletions pages/23_defense_picker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import streamlit as st
from pages_util.event_selector import event_selector
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()
st.title("Defense Picker")

st.subheader("Good Defense")

# Query to get all data

all_data = get_defense()


# Initialize session state to track hidden teams
if "hidden_teams" not in st.session_state:
st.session_state.hidden_teams = set()

# Step 1: Prepare data for the pill control (display team numbers)
all_teams = all_data['team_number'].unique().tolist()

# Step 2: Display the pill control for selecting teams
selected_teams = st.pills(
"Select Teams to Remove",
options=[str(team) for team in all_teams],
default=[str(team) for team in st.session_state.hidden_teams], # Pre-select hidden teams
selection_mode="multi"
)

# Step 3: Update the session state with the selected teams
# When teams are selected via the pills, we'll add those to the hidden_teams list
if selected_teams:
st.session_state.hidden_teams = set(map(int, selected_teams)) # Convert to integer if needed

# Step 4: Filter out the hidden teams for display
visible_data = all_data[~all_data['team_number'].isin(st.session_state.hidden_teams)]

# Step 5: Display the filtered DataFrame (visible teams only)
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
55 changes: 55 additions & 0 deletions pages/24_chat.py
Original file line number Diff line number Diff line change
@@ -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})
5 changes: 4 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,7 @@ scipy
pygwalker
schedule
duckdb==1.1.3
typer==0.4.2
typer==0.4.2
langchain
langchain_community
openai