Forgot to commit in small portions again

This commit is contained in:
2025-04-24 11:22:21 -04:00
parent 94084084f3
commit ef80aa825d
11 changed files with 679 additions and 3 deletions

4
.gitignore vendored
View File

@ -1,2 +1,4 @@
config.json
*/**/__pycache__
*/**/__pycache__
.venv
__main__

44
README.md Normal file
View File

@ -0,0 +1,44 @@
## Installation and Setup
1. **Prerequisites:**
* Python 3.8+
* [Ollama](https://ollama.com/)
2. **Clone the Repository:** (If you have not already, skip if you have the file contents)
3. **Create a Virtual Environment:**
```
python -m venv .venv
source venv/bin/activate # On Windows use `venv\Scripts\activate`
```
4. **Install Dependencies:**
```
pip install -r requirements.txt
```
5. **Set up Ollama:**
* Download and install Ollama from [https://ollama.com/](https://ollama.com/).
* Run `ollama pull <model_name>` to download the desired language model (e.g., `ollama pull llama2`).
## Configuration
1. **Configure:**
* The code uses `config.json.example` as a template. Copy this file to `config.json`.
* Edit `config.json` to provide
* The Blue Alliance (TBA) API key
* Indiana Scouting Alliance (ISA) API key
* Preferred Ollama model
## Running the Application
1. **Run the Main Script:**
```
python main.py
```
2. [**Open the page in your browser**](http://localhost:5000)

101
classes.puml Normal file
View File

@ -0,0 +1,101 @@
@startuml
' Data Sources
class DataSource {
{abstract} +get_status() : tuple[DataSourceStatus, dict]
{abstract} +get_team_info(team_number: int)
{abstract} +get_event_matches(event_code: str, team_number: int | None = None)
{abstract} +get_team_performance_metrics(team_number, event_code: str | None = None)
}
enum DataSourceStatus {
CONNECTED
UNAUTHENTICATED
NOT_FOUND
}
class TheBlueAllianceConnector {
+__init__(api_token: str, year: int = datetime.now().year)
+get_status() : tuple[DataSourceStatus, dict]
+get_team_info(team_number: int) : dict | None
+get_event_matches(event_code: str, team_number: int | None = None) : dict | None
+get_team_performance_metrics(team_number, event_code: str | None = None) : dict | None
-__calculate_auto_performance(performance: dict, match_points: dict, match_record: dict, alliance_data: dict, robot_position: int) : void
-__calculate_teleop_performance(performance: dict, match_points: dict, match_record: dict, alliance_data: dict, robot_position: int) : void
-__calculate_endgame_performance(performance: dict, match_points: dict, match_record: dict, alliance_data: dict, robot_position: int) : void
}
class IndianaScoutingAllianceConnector {
+__init__(api_token: str, year=datetime.now().year)
+get_status() : tuple[DataSourceStatus, dict]
+get_event_matches(event_code: str, team_number: int | None = None)
+get_robot_notes(team_number: int, event_code: str | None = None)
+get_team_info(team_number: int)
+get_team_performance_metrics(team_number, event_code: str | None = None)
-__build_ISA_robot_url(include_flags: str, teams: list = [], event_key: str = "") : str
-__build_ISA_human_url(include_flags: str, teams: list = [], event_key: str = "") : str
}
DataSourceStatus <|.. DataSource
DataSource <|-- TheBlueAllianceConnector
DataSource <|-- IndianaScoutingAllianceConnector
' LLM Integration
class AllianceSelectionAssistant {
+__init__(llm: OLLAMAConnector)
+select_alliance(teams: list, criteria: dict) : list
}
class OLLAMAConnector {
+__init__(model_name: str)
+generate_text(prompt: str) : str
}
class MatchPredictor {
+__init__(llm: OLLAMAConnector)
+predict_outcome(match_data: dict) : str
}
class TeamRatingGenerator {
+__init__(llm: OLLAMAConnector)
+rate_team(team_data: dict) : str
}
' Utils
class ConfigurationManager {
+get_config(key: str) : any
+set_config(key: str, value: any)
}
class Logger {
+__init__(name: str)
+log(message: str, level: str)
+info(message: str)
+warning(message: str)
+error(message: str)
+debug(message: str)
}
' Main
class FRCRatingApp {
+__init__()
+setup_routes() : void
+index()
+team_info(team_number: int)
+run(debug: bool = True) : void
}
' Relationships
AllianceSelectionAssistant --> OLLAMAConnector : Has
MatchPredictor --> OLLAMAConnector : Has
TeamRatingGenerator --> OLLAMAConnector : Has
FRCRatingApp --> ConfigurationManager : Uses
FRCRatingApp --> Logger : Uses
FRCRatingApp --> TheBlueAllianceConnector : Uses
FRCRatingApp --> IndianaScoutingAllianceConnector : Uses
FRCRatingApp --> AllianceSelectionAssistant : Uses
FRCRatingApp --> MatchPredictor : Uses
FRCRatingApp --> TeamRatingGenerator : Uses
@enduml

View File

@ -1,6 +1,6 @@
{
"TBA_TOKEN": "Get your read API token here: https://www.thebluealliance.com/account",
"USE_ISA_DATA": false,
"ISA_TOKEN": "If you are a member of the Indiana Scouting Alliance, put your token here. Reach out to the discord if you don't know how to get it",
"OLLAMA_MODEL": "dolphin-mixtral:latest"
"ISA_TOKEN": "If you are a member of the Indiana Scouting Alliance,put your token here. Reach out to the discord if you don't know how to get it",
"OLLAMA_MODEL": "llama2-uncensored:7b-chat-q2_K"
}

View File

@ -0,0 +1,15 @@
import json
class AllianceSelectionAssistant:
"""Assists in alliance selection using LLM and team data."""
def __init__(self, llm_model):
self.llm_model = llm_model
def generate_pick_list(self, teams_data, strategy):
teams_data_str = json.dumps(
teams_data
) # Serialize the list of dictionaries to a string
prompt = f"Given the following teams data: {teams_data_str} and pick strategy: {pick_strategy}, generate an alliance pick list."
return self.query_ollama(prompt)

View File

@ -0,0 +1,32 @@
import json
import requests
class OLLAMAConnector:
"""
Abstract class to interact with a local LLM using Ollama for predictions.
"""
def __init__(
self, model_name: str, ollama_base_url: str = "http://localhost:11434"
):
self.model_name = model_name
self.ollama_base_url = ollama_base_url
def query_ollama(self, prompt: str):
"""
Helper function to query the Ollama API.
"""
url = f"{self.ollama_base_url}/api/generate"
data = {
"prompt": prompt,
"model": self.model_name,
"stream": False, # Set to False to get the full response at once
}
try:
response = requests.post(url, json=data, stream=False)
response.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx)
return response.json()["response"]
except requests.exceptions.RequestException as e:
print(f"Error querying Ollama: {e}")
return None

View File

@ -0,0 +1,9 @@
class MatchPredictor:
"""Predicts match outcomes using LLM."""
def __init__(self, llm_model):
self.llm_model = llm_model
def predict_outcome(self, blue_alliance_data, red_alliance_data):
prompt = f"Given blue alliance data: {blue_alliance_data} and red alliance data: {red_alliance_data}, predict the match outcome."
return self.query_ollama(prompt)

View File

@ -0,0 +1,17 @@
class TeamRatingGenerator:
"""Generates subjective team ratings using LLM predictions"""
def __init__(self, llm_model):
self.llm_model = llm_model
def rate_team(
self,
perfomance_metrics: dict,
raw_event_data: dict,
isa_data: dict,
isa_notes: dict,
) -> str | None:
"""Rates a team based on available data"""
return self.llm_model.query_ollama(
prompt=f"The following First Robotics Competition (FRC), data comes from three different sources covering the exact same team and event, please cross reference them to identify possible problems. ```{perfomance_metrics}```, ```{raw_event_data}```, ```{isa_data}``` Do note that while the data source is the same for all three, the presentation of the data doesn't match up perfectly. I also have the following notes about the team in the data: ```{isa_notes}```Once you've done so, please use the data you have collected and referenced to give a comprehensive subjective rating to the team. This is not an interactive conversation, so please give an output that covers everything that you think the user may want in a single message including examples to support the conclusions. THE DATA ONLY CONTAINS ONE TEAM, OUTPUT MUST BE IN HTML FORMAT."
)

154
main.py Normal file
View File

@ -0,0 +1,154 @@
# from flask import Flask, render_template, jsonify
# from data_sources.tba import TheBlueAllianceConnector
# from data_sources.isa import IndianaScoutingAllianceConnector
# from llm_integration.llm_model import OLLAMAConnector
# from llm_integration.team_subjective_rating import TeamRatingGenerator
# from llm_integration.match_outcome_prediction import MatchPredictor
# from llm_integration.alliance_selection import AllianceSelectionAssistant
# from utils.config_manager import ConfigurationManager
# from utils.logger import Logger
# app = Flask(__name__)
# config = ConfigurationManager()
# logger = Logger(__name__)
# # Initialize data sources
# tba_api_key = config.get("TBA_TOKEN")
# tba_connector = TheBlueAllianceConnector(tba_api_key)
# isa_api_key = config.get("ISA_TOKEN")
# isa_connector = IndianaScoutingAllianceConnector(isa_api_key)
# # Initialize LLM model
# llm_model = config.get("OLLAMA_MODEL")
# llm = OLLAMAConnector(llm_model)
# # Initialize prediction and rating services
# team_rater = TeamRatingGenerator(llm)
# match_predictor = MatchPredictor(llm)
# alliance_assistant = AllianceSelectionAssistant(llm)
# @app.route("/")
# def index():
# return render_template("index.html")
# @app.route("/team/<int:team_number>")
# def team_info(team_number):
# event_code = "2025incmp"
# tba_team_performance_metrics = tba_connector.get_team_performance_metrics(
# team_number, event_code
# )
# tba_raw_event_data = tba_connector.get_event_matches(event_code, team_number)
# isa_data = isa_connector.get_event_matches(event_code, team_number)
# isa_notes = isa_connector.get_robot_notes(team_number, event_code)
# if tba_team_performance_metrics:
# # logger.info(f"Team {team_number} Metrics: {tba_team_performance_metrics}")
# # Generate subjective team rating
# logger.info(f"Generating Team rating...")
# team_rating = team_rater.rate_team(
# tba_team_performance_metrics, tba_raw_event_data, isa_data, isa_notes
# )
# output = f"Subjective Team Rating: {team_rating}"
# logger.info(output)
# return output
# else:
# output = (
# f"Could not retrieve metrics for team {team_number} at event {event_code}"
# )
# logger.info(output)
# return output
# if __name__ == "__main__":
# app.run(debug=True)
from flask import Flask, render_template, jsonify
from data_sources.tba import TheBlueAllianceConnector
from data_sources.isa import IndianaScoutingAllianceConnector
from llm_integration.llm_model import OLLAMAConnector
from llm_integration.team_subjective_rating import TeamRatingGenerator
from llm_integration.match_outcome_prediction import MatchPredictor
from llm_integration.alliance_selection import AllianceSelectionAssistant
from utils.config_manager import ConfigurationManager
from utils.logger import Logger
class FrcRatingApp:
def __init__(self):
self.app = Flask(__name__)
self.config = ConfigurationManager()
self.logger = Logger(__name__)
# Initialize data sources
tba_api_key = self.config.get("TBA_TOKEN")
self.tba_connector = TheBlueAllianceConnector(tba_api_key)
isa_api_key = self.config.get("ISA_TOKEN")
self.isa_connector = IndianaScoutingAllianceConnector(isa_api_key)
# Initialize LLM model
llm_model = self.config.get("OLLAMA_MODEL")
self.llm = OLLAMAConnector(llm_model)
# Initialize prediction and rating services
self.team_rater = TeamRatingGenerator(self.llm)
self.match_predictor = MatchPredictor(self.llm)
self.alliance_assistant = AllianceSelectionAssistant(self.llm)
self.setup_routes()
def setup_routes(self):
self.app.add_url_rule("/", "index", self.index)
self.app.add_url_rule("/team/<int:team_number>", "team_info", self.team_info)
def index(self):
return render_template("index.html")
def team_info(self, team_number):
event_code = "2025incmp"
tba_team_performance_metrics = self.tba_connector.get_team_performance_metrics(
team_number, event_code
)
tba_raw_event_data = self.tba_connector.get_event_matches(
event_code, team_number
)
isa_data = self.isa_connector.get_event_matches(event_code, team_number)
isa_notes = self.isa_connector.get_robot_notes(team_number, event_code)
if tba_team_performance_metrics:
# Generate subjective team rating
self.logger.info(f"Generating Team rating...")
team_rating = self.team_rater.rate_team(
tba_team_performance_metrics,
tba_raw_event_data,
isa_data,
isa_notes,
)
output = f"Subjective Team Rating: {team_rating}"
self.logger.info(output)
return output
else:
output = f"Could not retrieve metrics for team {team_number} at event {event_code}"
self.logger.info(output)
return output
def run(self, debug=True):
self.app.run(debug=debug)
if __name__ == "__main__":
frc_rating_app = FrcRatingApp()
frc_rating_app.run()

12
requirements.txt Normal file
View File

@ -0,0 +1,12 @@
blinker==1.9.0
certifi==2025.1.31
charset-normalizer==3.4.1
click==8.1.8
Flask==3.1.0
idna==3.10
itsdangerous==2.2.0
Jinja2==3.1.6
MarkupSafe==3.0.2
requests==2.32.3
urllib3==2.4.0
Werkzeug==3.1.3

290
templates/index.html Normal file
View File

@ -0,0 +1,290 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Team Info Lookup</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap 4 CDN for quick, nice styling -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
<style>
body {
background: #f8f9fa;
}
.main-container {
min-width: 50%;
max-width: 90%;
margin: 60px auto;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.07);
padding: 32px 24px 24px 24px;
}
.result-box {
margin-top: 32px;
padding: 20px;
border-radius: 6px;
background: #e9ecef;
min-height: 60px;
}
.navbar-brand {
font-weight: bold;
letter-spacing: 1px;
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="#">Team Lookup</a>
</nav>
<div class="main-container">
<h2 class="mb-4 text-center">Lookup FRC Team Info</h2>
<form id="team-form">
<div class="form-group">
<label for="team-number">Enter Team Number</label>
<input type="number" class="form-control" id="team-number" name="team_number" min="1" required>
</div>
<button type="submit" class="btn btn-primary btn-block">Get Team Info</button>
</form>
<h2>The following result is generated using an LLM, please note that anything said in the following block of
text does not reflect my own personal views and I do not vouch for it's accuracy.</h2>
<div id="result" class="result-box mt-4" style="display:none;"></div>
<div class="team-list-section">
<h4 class="mb-3">I have data about the following teams:</h4>
<div class="table-responsive">
<table class="table table-striped team-table">
<thead class="thead-dark">
<tr>
<th scope="col">Team</th>
<th scope="col">Name</th>
<th scope="col">Location</th>
</tr>
</thead>
<tbody>
<tr>
<td>45</td>
<td>TechnoKats Robotics Team</td>
<td>Kokomo, Indiana, USA</td>
</tr>
<tr>
<td>71</td>
<td>Team Hammond</td>
<td>Hammond, Indiana, USA</td>
</tr>
<tr>
<td>135</td>
<td>Penn Robotics Black Knights</td>
<td>Mishawaka, Indiana, USA</td>
</tr>
<tr>
<td>234</td>
<td>Cyber Blue</td>
<td>Indianapolis, Indiana, USA</td>
</tr>
<tr>
<td>292</td>
<td>PantherTech</td>
<td>Russiaville, Indiana, USA</td>
</tr>
<tr>
<td>328</td>
<td>Penn Robotics Golden Rooks</td>
<td>Mishawaka, Indiana, USA</td>
</tr>
<tr>
<td>447</td>
<td>Team Roboto</td>
<td>Anderson, Indiana, USA</td>
</tr>
<tr>
<td>461</td>
<td>Westside Boiler Invasion</td>
<td>West Lafayette, Indiana, USA</td>
</tr>
<tr>
<td>829</td>
<td>The Digital Goats</td>
<td>Indianapolis, Indiana, USA</td>
</tr>
<tr>
<td>868</td>
<td>TechHOUNDS</td>
<td>Carmel, Indiana, USA</td>
</tr>
<tr>
<td>1018</td>
<td>Pike RoboDevils</td>
<td>Indianapolis, Indiana, USA</td>
</tr>
<tr>
<td>1024</td>
<td>Kil-A-Bytes</td>
<td>Indianapolis, Indiana, USA</td>
</tr>
<tr>
<td>1501</td>
<td>Team THRUST</td>
<td>Huntington, Indiana, USA</td>
</tr>
<tr>
<td>1741</td>
<td>Red Alert</td>
<td>Greenwood, Indiana, USA</td>
</tr>
<tr>
<td>1747</td>
<td>Harrison Boiler Robotics</td>
<td>West Lafayette, Indiana, USA</td>
</tr>
<tr>
<td>2171</td>
<td>RoboDogs</td>
<td>Crown Point, Indiana, USA</td>
</tr>
<tr>
<td>2197</td>
<td>Las Pumas</td>
<td>New Carlisle, Indiana, USA</td>
</tr>
<tr>
<td>3176</td>
<td>Purple Precision</td>
<td>Brownsburg, Indiana, USA</td>
</tr>
<tr>
<td>3487</td>
<td>Red Pride Robotics</td>
<td>Plainfield, Indiana, USA</td>
</tr>
<tr>
<td>3494</td>
<td>The Quadrangles</td>
<td>Bloomington, Indiana, USA</td>
</tr>
<tr>
<td>3940</td>
<td>CyberTooth</td>
<td>Kokomo, Indiana, USA</td>
</tr>
<tr>
<td>4272</td>
<td>Maverick Robotics</td>
<td>Lafayette, Indiana, USA</td>
</tr>
<tr>
<td>4485</td>
<td>Tribe Tech Robotics</td>
<td>Danville, Indiana, USA</td>
</tr>
<tr>
<td>4926</td>
<td>GalacTech</td>
<td>Columbus, Indiana, USA</td>
</tr>
<tr>
<td>5010</td>
<td>Tiger Dynasty</td>
<td>Fishers, Indiana, USA</td>
</tr>
<tr>
<td>5188</td>
<td>Area 5188: Classified Robotics</td>
<td>Terre Haute, Indiana, USA</td>
</tr>
<tr>
<td>5402</td>
<td>Wreckless Robotics</td>
<td>Loganpsport/Walton, Indiana, USA</td>
</tr>
<tr>
<td>5484</td>
<td>Career Academy Robotics - Wolf Pack</td>
<td>South Bend, Indiana, USA</td>
</tr>
<tr>
<td>6721</td>
<td>Tindley Trailblazers</td>
<td>Indianapolis, Indiana, USA</td>
</tr>
<tr>
<td>6956</td>
<td>SHAM-ROCK-BOTICS ☘</td>
<td>Westfield, Indiana, USA</td>
</tr>
<tr>
<td>7454</td>
<td>Huskies on Hogs</td>
<td>Evansville, Indiana, USA</td>
</tr>
<tr>
<td>7457</td>
<td>suPURDUEper Robotics</td>
<td>Indianapolis, Indiana, USA</td>
</tr>
<tr>
<td>7617</td>
<td>RoboBlazers</td>
<td>Carmel, Indiana, USA</td>
</tr>
<tr>
<td>7657</td>
<td>ThunderBots</td>
<td>Evansville, Indiana, USA</td>
</tr>
<tr>
<td>8103</td>
<td>Knight Robotics</td>
<td>Kendallville, Indiana, USA</td>
</tr>
<tr>
<td>8430</td>
<td>The Hatch Batch</td>
<td>Washington, Indiana, USA</td>
</tr>
<tr>
<td>10021</td>
<td>Guerin Catholic Golden Gears</td>
<td>Noblesville, Indiana, USA</td>
</tr>
<tr>
<td>10332</td>
<td>Carroll Charger Robotics</td>
<td>Fort Wayne, Indiana, USA</td>
</tr>
<tr>
<td>10492</td>
<td>Bosse BYTEForce</td>
<td>Evansville, Indiana, USA</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<script>
document.getElementById('team-form').addEventListener('submit', function (e) {
e.preventDefault();
const teamNumber = document.getElementById('team-number').value;
const resultDiv = document.getElementById('result');
resultDiv.style.display = 'block';
resultDiv.innerHTML = '<div class="text-center text-muted">Loading...</div>';
fetch(`/team/${teamNumber}`, { signal: AbortSignal.timeout(50000000000) })
.then(response => response.text())
.then(rating => {
resultDiv.innerHTML = rating;
})
.catch(err => {
resultDiv.innerHTML = `<div class="alert alert-danger">Failed to fetch team info.</div>`;
});
});
</script>
</body>
</html>