Skip to content
Closed
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
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ pydantic==2.7.1
python-dotenv==0.19.2
sqlalchemy==1.4.52
uvicorn==0.29.0
openai==1.6.0
15 changes: 15 additions & 0 deletions server/controllers/text_parser_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from sqlalchemy.ext.asyncio import AsyncSession
from fastapi import HTTPException
from services.text_parser_service import TextParserService
from schemas.schemas import TextFileCreate


class TextParserController:
def __init__(self, db: AsyncSession):
self.service = TextParserService(db)

async def parse_speech_text(self, text_file: TextFileCreate):
try:
return await self.service.parse_text_file(text_file)
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
10 changes: 6 additions & 4 deletions server/database/setup_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,14 @@
cursor.execute('''
CREATE TABLE IF NOT EXISTS games (
id TEXT PRIMARY KEY,
hometeam_id TEXT,
awayteam_id TEXT,
home_team_id TEXT,
away_team_id TEXT,
home_team_score INTEGER,
away_team_score INTEGER,
updated_at TIMESTAMP,
created_at TIMESTAMP,
FOREIGN KEY (hometeam_id) REFERENCES teams(id),
FOREIGN KEY (awayteam_id) REFERENCES teams(id)
FOREIGN KEY (home_team_id) REFERENCES teams(id),
FOREIGN KEY (away_team_id) REFERENCES teams(id)
)
''')
print("Created games table")
Expand Down Expand Up @@ -176,11 +176,13 @@
CREATE TABLE IF NOT EXISTS text_file (
id TEXT PRIMARY KEY,
user_id TEXT,
game_id TEXT,
audio_id TEXT,
contents TEXT,
created_at TIMESTAMP,
file_path TEXT,
FOREIGN KEY (audio_id) REFERENCES audio_file(id),
FOREIGN KEY (game_id) REFERENCES games(id),
FOREIGN KEY (user_id) REFERENCES users(id)
)
''')
Expand Down
3 changes: 3 additions & 0 deletions server/models/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class Game(Base):
away_team = relationship("Team", foreign_keys=[away_team_id], back_populates="away_games")
team_stats = relationship("TeamStats", back_populates="game")
player_stats = relationship("PlayerStats", back_populates="game")
text_files = relationship("TextFile", back_populates="game")


class Player(Base):
Expand Down Expand Up @@ -160,7 +161,9 @@ class TextFile(Base):
contents = Column(Text)
created_at = Column(TIMESTAMP)
file_path = Column(String)
game_id = Column(String, ForeignKey('games.id'))
user_id = Column(String, ForeignKey('users.id'))

game = relationship("Game", back_populates="text_files")
user = relationship("User", back_populates="text_files")
audio_file = relationship("AudioFile", back_populates="text_files")
20 changes: 20 additions & 0 deletions server/routes/text_parser_router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession

from database.config import get_db
from schemas.schemas import TextFileCreate


parser_router = APIRouter()


@parser_router.post("/speech_text_command/")
async def speech_text_command(text_file: TextFileCreate, db: AsyncSession = Depends(get_db)):
try:
from controllers.text_parser_controller import TextParserController
msg = await TextParserController(db).parse_speech_text(text_file)
except Exception as e:
msg = str(e)
return {
"message": msg,
}
15 changes: 8 additions & 7 deletions server/schemas/schemas.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from pydantic import BaseModel, EmailStr
from typing import List, Optional
from typing import Optional
from datetime import datetime


Expand Down Expand Up @@ -92,9 +92,9 @@ class TeamStatsBase(BaseModel):
points: int
rebounds: int
assists: int
steal: int
block: int
turnover: int
steals: int
blocks: int
turnovers: int
fouls: int
fg_made: int
fg_attempt: int
Expand Down Expand Up @@ -162,9 +162,9 @@ class PlayerStatsBase(BaseModel):
points: int
rebounds: int
assists: int
steal: int
block: int
turnover: int
steals: int
blocks: int
turnovers: int
fouls: int
fg_made: int
fg_attempt: int
Expand Down Expand Up @@ -211,6 +211,7 @@ class Config:
class TextFileBase(BaseModel):
id: str
audio_id: str
game_id: str
contents: str
file_path: str

Expand Down
2 changes: 2 additions & 0 deletions server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@

from middleware import cors_middleware
from routes.game_router import game_router
from routes.text_parser_router import parser_router


try:
app = FastAPI()
cors_middleware.add(app)
app.include_router(game_router)
app.include_router(parser_router)
uvicorn.run(app, port=9091)
except KeyboardInterrupt as e:
pass
Expand Down
26 changes: 25 additions & 1 deletion server/services/game_service.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Union
from typing import Union, List

from sqlalchemy.ext.asyncio import AsyncSession
from models.models import Game, Player, PlayerStats, Team, TeamStats
Expand Down Expand Up @@ -32,12 +32,20 @@ async def create_player(self, player_data: PlayerCreate) -> Player:
new_player = await self.create(Player(**player_data.dict()))
return new_player

async def fetch_game(self, game_id: str) -> Game:
return await self.db \
.query(Game) \
.filter(Game.game_id == game_id) \
.scalars() \
.first()

async def fetch_player_stats(self, player_id: str, game_id: str) -> PlayerStats:
return await self.db \
.query(PlayerStats) \
.filter(
PlayerStats.player_id == player_id,
PlayerStats.game_id == game_id) \
.scalars() \
.first()

async def fetch_team_stats(self, team_id: str, game_id: str) -> TeamStats:
Expand All @@ -46,8 +54,23 @@ async def fetch_team_stats(self, team_id: str, game_id: str) -> TeamStats:
.filter(
TeamStats.team_id == team_id,
TeamStats.game_id == game_id) \
.scalars() \
.first()

async def fetch_team(self, team_id: str) -> Team:
return await self.db \
.query(Team) \
.filter(Team.id == team_id) \
.scalars() \
.first()

async def fetch_players(self, team_id: str) -> List[Player]:
return await self.db \
.query(Player) \
.filter(Player.team_id == team_id) \
.scalars() \
.all()

async def update_player_stats(self, stats_update: PlayerStatsUpdate):
player_stats = await self.fetch_player_stats(stats_update.player_id)
if player_stats:
Expand All @@ -58,4 +81,5 @@ async def update_player_stats(self, stats_update: PlayerStatsUpdate):
stat_to_update,
getattr(player_stats, stat_to_update) + value
)
await self.db.add(player_stats)
await self.db.commit()
134 changes: 134 additions & 0 deletions server/services/text_parser_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import ast
import os
from datetime import datetime
from dotenv import load_dotenv
from openai import AzureOpenAI
from typing import List
from sqlalchemy.ext.asyncio import AsyncSession

from models.models import Game, TextFile
from schemas.schemas import TextFileCreate
from services.game_service import GameService

load_dotenv()


API_KEY = os.getenv("AZURE_API_KEY")
ENDPOINT = os.getenv("AZURE_PROXY_ENDPOINT")
API_VERSION = "2024-02-01"
MODEL_NAME = "gpt-35-turbo"
SYSTEM_CONTENT = """
You are a box score keeper for a basketball game.

You will be given a sentence and need to parse the input into the following format:
-> [[TEAM_NAME, PLAYER_NUMBER, STAT_ACTION, STAT_VALUE], ...]

Do not add more content beyond the format and exa mple outputs.

Example 1
Input: John got a rebound
Output: [[Raptors, 12, rebounds, 1]]
Explanation: John (#12) from Raptors got a rebound
Example 2
Input: Warriors player Steve gave an assist to John, and player John made a three point shot
Output: [[Warriors, 1, assists, 1], [Warriors, 3, three_pts_made, 1]]
Explanation: Steve (#1) from Warriors gave an assist to John (#3) from the same team, and John (#3) made a three-point shot
Example 3
Input: Mike from Raptors missed a shot
Output: [[Raptors, 23, fg_attempt, 1]]
Explanation: Mike (#23) from Raptors missed a field goal attempt
Example 4
Input: Boston player Dave blocked a shot
Output: [[Boston, 17, blocks, 1]]
Explanation: Dave (#17) from Boston blocked a shot
Example 5
Input: Kobe Bryant stole the ball from Paul Piece
Output: [[Lakers, 24, steals, 1], [Boston, 34, turnovers, 1]]
Explanation: Kobe Bryant (#24) from Lakers and hence Paul Pierce (#34) from Boston has a turnover

You may refer to the following supporting content to make sure outputs are valid.
Sometimes only players name/number are given. Refer to the supporting content to fill the output.

Supporting content for parsing the user input texts:
Stats to fill: [rebounds,assists,steals,blocks,turnovers,fouls,fg_made,fg_attempt,ft_made,ft_attempt,three_pts_made,three_pts_attempt]
"""


class TextParserService:
db: AsyncSession
client: AzureOpenAI

def __init__(self, db: AsyncSession):
self.db = db
self.game_service = GameService(db)
self.openai_client = AzureOpenAI(
azure_endpoint=ENDPOINT,
api_key=API_KEY,
api_version=API_VERSION,
)

def completion_request(self, message) -> List[List]:
completion = self.openai_client.chat.completions.create(
model=MODEL_NAME,
messages=message,
)
results = ast.literal_eval((completion.choices[0].message.content))

return results

async def parse_text_file(self, text_file: TextFileCreate) -> List[List]:
self.save_text_file(text_file)

# request
message = await self.build_messages(text_file)
results = self.completion_request(message)

if self.check_results(results):
return results

print(f"The output was invalid: {results}")
return [[]]

async def save_text_file(self, text_file: TextFileCreate):
now = datetime.now()
new_entry = TextFile(**text_file.dict())
new_entry.id = f"textfile_{text_file.game_id}_{now.strftime('%H%M%S')}"
new_entry.created_at = now
self.db.add(new_entry)
await self.db.commit()
await self.db.refresh(new_entry)

async def build_messages(self, text_file: TextFileCreate):
game = await self.game_service.fetch_game(text_file.game_id)
supporting_content = await self.build_supporting_content(game)
content = SYSTEM_CONTENT + "\n" + supporting_content
return [
{"role": "system", "content": content},
{"role": "user", "content": text_file.contents}
]

async def build_supporting_content(self, game: Game) -> str:
home_team = await self.game_service.fetch_team(game.home_team_id)
home_team_players = await self.game_service.fetch_players(game.home_team_id)
away_team = await self.game_service.fetch_team(game.away_team_id)
away_team_players = await self.game_service.fetch_players(game.away_team_id)

supporting_content = f"Home team name: {home_team.name}\n"
supporting_content += f"Home team players: {[(player.name, player.number) for player in home_team_players]}\n"
supporting_content += f"Away team name: {away_team.name}\n"
supporting_content += f"Away team players: {[(player.name, player.number) for player in away_team_players]}\n"

return supporting_content

def check_results(self, results: List) -> bool:
is_valid_team_name = is_valid_player_number = is_valid_stat = is_valid_value = False\

for result in results:
team_name, player_number, stat, value = result
is_valid_team_name = isinstance(team_name, str)
is_valid_player_number = isinstance(player_number, int)
is_valid_stat = isinstance(stat, str)
is_valid_value = isinstance(value, int)

return is_valid_team_name and is_valid_player_number \
and is_valid_stat and is_valid_value