Coverage for chatgpt_proxy / db / models.py: 99%
98 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-12 16:19 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-12 16:19 +0000
1# MIT License
2#
3# Copyright (c) 2025 Tuomo Kriikkula
4#
5# Permission is hereby granted, free of charge, to any person obtaining a copy
6# of this software and associated documentation files (the "Software"), to deal
7# in the Software without restriction, including without limitation the rights
8# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9# copies of the Software, and to permit persons to whom the Software is
10# furnished to do so, subject to the following conditions:
11#
12# The above copyright notice and this permission notice shall be included in all
13# copies or substantial portions of the Software.
14#
15# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21# SOFTWARE.
23"""Read-only models for chatgpt_proxy's 'adhoc ORM'.
25TODO: consider replacing these with asyncpg.Record
26 derived classes?
27"""
29import ast
30import datetime
31import ipaddress
32from dataclasses import dataclass
33from enum import StrEnum
35max_ast_literal_eval_size = 1000
38# Mirrored in ChatGPTBotsMutator.uc!
39class SayType(StrEnum):
40 ALL = "0"
41 TEAM = "1"
44# TODO: for LLM prompting purposes, we could have some
45# type to refer to US Army, PAVN, NLF, etc.!
46class Team(StrEnum):
47 North = "0"
48 South = "1"
49 Neutral = "3"
52@dataclass(slots=True, frozen=True)
53class Game:
54 id: str
55 level: str
56 start_time: datetime.datetime
57 game_server_address: ipaddress.IPv4Address
58 game_server_port: int
59 stop_time: datetime.datetime | None = None
60 openai_previous_response_id: str | None = None
63@dataclass(slots=True, frozen=True)
64class GamePlayer:
65 game_id: str
66 id: int
67 name: str
68 team: Team
69 score: int
71 def as_markdown_dict(self) -> dict:
72 return {
73 "Name": self.name,
74 "Team:": self.team.name,
75 "Score:": self.score,
76 }
78 def wire_format(self) -> str:
79 return f"{self.name}\n{self.team}\n{self.score}"
82@dataclass(slots=True, frozen=True)
83class OpenAIQuery:
84 time: datetime.datetime
85 game_id: str
86 game_server_address: ipaddress.IPv4Address
87 game_server_port: int
88 request_length: int
89 response_length: int
90 openai_response_id: str
93@dataclass(slots=True, frozen=True)
94class GameChatMessage:
95 id: int
96 message: str
97 game_id: str
98 send_time: datetime.datetime
99 sender_name: str
100 sender_team: Team
101 channel: SayType
103 def as_markdown_dict(self) -> dict:
104 return {
105 "Message": self.message,
106 "Sender:": self.sender_name,
107 "Team:": self.sender_team,
108 "Channel:": self.channel,
109 }
111 def wire_format(self) -> str:
112 return f"{self.sender_name}\n{self.sender_team}\n{self.channel}\n{self.message}"
115@dataclass(slots=True, frozen=True)
116class GameObjective:
117 name: str
118 team_state: Team
120 # TODO: this is wrong.
121 # - maybe add custom __str__
122 def wire_format(self) -> str:
123 return f"('{self.name}',{int(self.team_state)})"
126@dataclass(slots=True, frozen=True)
127class GameObjectiveState:
128 game_id: str
129 objectives: list[GameObjective]
131 def wire_format(self) -> str:
132 # TODO: too many quotes with this method!
133 return str([(obj.name, int(obj.team_state)) for obj in self.objectives])
135 @staticmethod
136 def from_wire_format(
137 game_id: str,
138 wire_format_data: str,
139 ) -> "GameObjectiveState":
140 if len(wire_format_data) > max_ast_literal_eval_size:
141 raise ValueError("wire_format_data too long")
143 raw_objs: list[tuple[str, int]] = ast.literal_eval(wire_format_data)
145 t = type(raw_objs)
146 if t is not list:
147 raise ValueError(f"objs: expected list type, got {t}")
149 objs = []
150 for obj_name, obj_state in raw_objs:
151 if type(obj_name) is not str:
152 raise ValueError(f"obj_name: expected str type, got {type(obj_name)}")
153 if type(obj_state) is not int:
154 raise ValueError(f"obj_state: expected int type, got {type(obj_state)}")
156 obj_state_enum = Team(str(obj_state))
158 objs.append(GameObjective(
159 name=obj_name,
160 team_state=obj_state_enum,
161 ))
163 return GameObjectiveState(
164 game_id=game_id,
165 objectives=objs,
166 )
169@dataclass(slots=True, frozen=True)
170class GameKill:
171 id: int
172 game_id: str
173 kill_time: datetime.datetime
174 killer_name: str
175 victim_name: str
176 killer_team: Team
177 victim_team: Team
178 damage_type: str
179 kill_distance_m: float
181 def as_markdown_dict(self) -> dict:
182 # TODO: also do this for other well known damage type prefixes?
183 dmg_type = self.damage_type.replace("RODmgType_", "")
184 return {
185 "Killer": self.killer_name,
186 "Victim": self.victim_name,
187 "Killer Team:": self.killer_team,
188 "Victim Team:": self.victim_team,
189 "Damage Type:": dmg_type,
190 "Kill Distance (m):": round(self.kill_distance_m, 1),
191 }