feat: Migrate Gemini SDK to google-genai (#231) #236
@@ -87,7 +87,7 @@ export const vehiclesApi = {
|
|||||||
*/
|
*/
|
||||||
decodeVin: async (vin: string): Promise<DecodedVehicleData> => {
|
decodeVin: async (vin: string): Promise<DecodedVehicleData> => {
|
||||||
const response = await apiClient.post('/vehicles/decode-vin', { vin }, {
|
const response = await apiClient.post('/vehicles/decode-vin', { vin }, {
|
||||||
timeout: 60000 // 60 seconds for Gemini cold start
|
timeout: 120000 // 120 seconds for Gemini + Google Search grounding
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,8 +40,9 @@ Return the results as a JSON object with a single "maintenanceSchedule" array.\
|
|||||||
|
|
||||||
# VIN year code lookup: position 10 character -> base year (first cycle, 1980-2009).
|
# VIN year code lookup: position 10 character -> base year (first cycle, 1980-2009).
|
||||||
# The 30-year cycle repeats: +30 for 2010-2039, +60 for 2040-2069.
|
# The 30-year cycle repeats: +30 for 2010-2039, +60 for 2040-2069.
|
||||||
# Disambiguation uses position 7: numeric -> 2010+ cycle, alphabetic -> 1980s cycle.
|
# Disambiguation uses position 7: alphabetic -> 2010+ cycle, numeric -> 1980s cycle.
|
||||||
# For the 2040+ cycle (when position 7 is alphabetic again), we pick the most
|
# Per NHTSA FMVSS No. 115: MY2010+ vehicles must use alphabetic position 7.
|
||||||
|
# For the 2040+ cycle (when position 7 is numeric again), we pick the most
|
||||||
# recent plausible year (not more than 2 years in the future).
|
# recent plausible year (not more than 2 years in the future).
|
||||||
_VIN_YEAR_CODES: dict[str, int] = {
|
_VIN_YEAR_CODES: dict[str, int] = {
|
||||||
"A": 1980, "B": 1981, "C": 1982, "D": 1983, "E": 1984,
|
"A": 1980, "B": 1981, "C": 1982, "D": 1983, "E": 1984,
|
||||||
@@ -58,10 +59,10 @@ def resolve_vin_year(vin: str) -> int | None:
|
|||||||
"""Deterministically resolve model year from VIN positions 7 and 10.
|
"""Deterministically resolve model year from VIN positions 7 and 10.
|
||||||
|
|
||||||
VIN year codes repeat on a 30-year cycle. Position 7 disambiguates:
|
VIN year codes repeat on a 30-year cycle. Position 7 disambiguates:
|
||||||
- Numeric position 7 -> 2010-2039 cycle
|
- Alphabetic position 7 -> 2010-2039 cycle (NHTSA MY2010+ requirement)
|
||||||
- Alphabetic position 7 -> 1980-2009 or 2040-2050+ cycle
|
- Numeric position 7 -> 1980-2009 or 2040-2069 cycle
|
||||||
|
|
||||||
For the alphabetic case with three possible cycles, picks the most recent
|
For the numeric case with two possible cycles, picks the most recent
|
||||||
year that is not more than 2 years in the future.
|
year that is not more than 2 years in the future.
|
||||||
|
|
||||||
Returns None if the VIN is too short or position 10 is not a valid year code.
|
Returns None if the VIN is too short or position 10 is not a valid year code.
|
||||||
@@ -76,11 +77,11 @@ def resolve_vin_year(vin: str) -> int | None:
|
|||||||
if base_year is None:
|
if base_year is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if pos7.isdigit():
|
if pos7.isalpha():
|
||||||
# Numeric position 7 -> second cycle (2010-2039)
|
# Alphabetic position 7 -> second cycle (2010-2039)
|
||||||
return base_year + 30
|
return base_year + 30
|
||||||
|
|
||||||
# Alphabetic position 7 -> first cycle (1980-2009) or third cycle (2040-2069)
|
# Numeric position 7 -> first cycle (1980-2009) or third cycle (2040-2069)
|
||||||
# Pick the most recent plausible year
|
# Pick the most recent plausible year
|
||||||
max_plausible = datetime.now().year + 2
|
max_plausible = datetime.now().year + 2
|
||||||
|
|
||||||
@@ -381,6 +382,9 @@ class GeminiEngine:
|
|||||||
response_mime_type="application/json",
|
response_mime_type="application/json",
|
||||||
response_schema=_VIN_DECODE_SCHEMA,
|
response_schema=_VIN_DECODE_SCHEMA,
|
||||||
tools=[types.Tool(google_search=types.GoogleSearch())],
|
tools=[types.Tool(google_search=types.GoogleSearch())],
|
||||||
|
automatic_function_calling=types.AutomaticFunctionCallingConfig(
|
||||||
|
max_remote_calls=3,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -12,86 +12,86 @@ from app.engines.gemini_engine import resolve_vin_year
|
|||||||
|
|
||||||
|
|
||||||
class TestSecondCycle:
|
class TestSecondCycle:
|
||||||
"""Position 7 numeric -> 2010-2039 cycle."""
|
"""Position 7 alphabetic -> 2010-2039 cycle (NHTSA MY2010+ requirement)."""
|
||||||
|
|
||||||
def test_p_with_numeric_pos7_returns_2023(self):
|
def test_p_with_alpha_pos7_returns_2023(self):
|
||||||
"""P=2023 when position 7 is numeric (the bug that triggered this fix)."""
|
"""P=2023 when position 7 is alphabetic (the bug that triggered this fix)."""
|
||||||
# VIN: 1G1YE2D32P5602473 -- pos7='2' (numeric), pos10='P'
|
# VIN: 1G1YE2D32P5602473 -- pos7='D' (alphabetic), pos10='P'
|
||||||
assert resolve_vin_year("1G1YE2D32P5602473") == 2023
|
assert resolve_vin_year("1G1YE2D32P5602473") == 2023
|
||||||
|
|
||||||
def test_a_with_numeric_pos7_returns_2010(self):
|
def test_a_with_alpha_pos7_returns_2010(self):
|
||||||
"""A=2010 when position 7 is numeric."""
|
"""A=2010 when position 7 is alphabetic."""
|
||||||
assert resolve_vin_year("1G1YE2112A5602473") == 2010
|
assert resolve_vin_year("1G1YE2D12A5602473") == 2010
|
||||||
|
|
||||||
def test_l_with_numeric_pos7_returns_2020(self):
|
def test_l_with_alpha_pos7_returns_2020(self):
|
||||||
"""L=2020 when position 7 is numeric."""
|
"""L=2020 when position 7 is alphabetic."""
|
||||||
assert resolve_vin_year("1G1YE2112L5602473") == 2020
|
assert resolve_vin_year("1G1YE2D12L5602473") == 2020
|
||||||
|
|
||||||
def test_9_with_numeric_pos7_returns_2039(self):
|
def test_9_with_alpha_pos7_returns_2039(self):
|
||||||
"""9=2039 when position 7 is numeric."""
|
"""9=2039 when position 7 is alphabetic."""
|
||||||
assert resolve_vin_year("1G1YE211295602473") == 2039
|
assert resolve_vin_year("1G1YE2D1295602473") == 2039
|
||||||
|
|
||||||
def test_digit_1_with_numeric_pos7_returns_2031(self):
|
def test_digit_1_with_alpha_pos7_returns_2031(self):
|
||||||
"""1=2031 when position 7 is numeric."""
|
"""1=2031 when position 7 is alphabetic."""
|
||||||
assert resolve_vin_year("1G1YE211215602473") == 2031
|
assert resolve_vin_year("1G1YE2D1215602473") == 2031
|
||||||
|
|
||||||
def test_s_with_numeric_pos7_returns_2025(self):
|
def test_s_with_alpha_pos7_returns_2025(self):
|
||||||
"""S=2025 when position 7 is numeric."""
|
"""S=2025 when position 7 is alphabetic."""
|
||||||
assert resolve_vin_year("1G1YE2112S5602473") == 2025
|
assert resolve_vin_year("1G1YE2D12S5602473") == 2025
|
||||||
|
|
||||||
def test_t_with_numeric_pos7_returns_2026(self):
|
def test_t_with_alpha_pos7_returns_2026(self):
|
||||||
"""T=2026 when position 7 is numeric."""
|
"""T=2026 when position 7 is alphabetic."""
|
||||||
assert resolve_vin_year("1G1YE2112T5602473") == 2026
|
assert resolve_vin_year("1G1YE2D12T5602473") == 2026
|
||||||
|
|
||||||
|
|
||||||
class TestFirstCycle:
|
class TestFirstCycle:
|
||||||
"""Position 7 alphabetic -> 1980-2009 cycle (when 2040+ is not yet plausible)."""
|
"""Position 7 numeric -> 1980-2009 cycle."""
|
||||||
|
|
||||||
def test_m_with_alpha_pos7_returns_1991(self):
|
def test_m_with_numeric_pos7_returns_1991(self):
|
||||||
"""M=1991 when position 7 is alphabetic (third cycle 2051 is not plausible)."""
|
"""M=1991 when position 7 is numeric."""
|
||||||
assert resolve_vin_year("1G1YE2J32M5602473") == 1991
|
assert resolve_vin_year("1G1YE2132M5602473") == 1991
|
||||||
|
|
||||||
def test_n_with_alpha_pos7_returns_1992(self):
|
def test_n_with_numeric_pos7_returns_1992(self):
|
||||||
"""N=1992 when position 7 is alphabetic."""
|
"""N=1992 when position 7 is numeric."""
|
||||||
assert resolve_vin_year("1G1YE2J32N5602473") == 1992
|
assert resolve_vin_year("1G1YE2132N5602473") == 1992
|
||||||
|
|
||||||
def test_p_with_alpha_pos7_returns_1993(self):
|
def test_p_with_numeric_pos7_returns_1993(self):
|
||||||
"""P=1993 when position 7 is alphabetic (third cycle 2053 not plausible)."""
|
"""P=1993 when position 7 is numeric."""
|
||||||
assert resolve_vin_year("1G1YE2J32P5602473") == 1993
|
assert resolve_vin_year("1G1YE2132P5602473") == 1993
|
||||||
|
|
||||||
def test_y_with_alpha_pos7_returns_2000(self):
|
def test_y_with_numeric_pos7_returns_2000(self):
|
||||||
"""Y=2000 when position 7 is alphabetic."""
|
"""Y=2000 when position 7 is numeric."""
|
||||||
assert resolve_vin_year("1G1YE2J32Y5602473") == 2000
|
assert resolve_vin_year("1G1YE2132Y5602473") == 2000
|
||||||
|
|
||||||
|
|
||||||
class TestThirdCycle:
|
class TestThirdCycle:
|
||||||
"""Position 7 alphabetic + third cycle year (2040-2050) is plausible."""
|
"""Position 7 numeric + third cycle year (2040-2050) is plausible."""
|
||||||
|
|
||||||
@patch("app.engines.gemini_engine.datetime")
|
@patch("app.engines.gemini_engine.datetime")
|
||||||
def test_a_with_alpha_pos7_returns_2040_when_plausible(self, mock_dt):
|
def test_a_with_numeric_pos7_returns_2040_when_plausible(self, mock_dt):
|
||||||
"""A=2040 when position 7 is alphabetic and year 2040 is plausible."""
|
"""A=2040 when position 7 is numeric and year 2040 is plausible."""
|
||||||
mock_dt.now.return_value = datetime(2039, 1, 1)
|
mock_dt.now.return_value = datetime(2039, 1, 1)
|
||||||
# 2039 + 2 = 2041 >= 2040, so third cycle is plausible
|
# 2039 + 2 = 2041 >= 2040, so third cycle is plausible
|
||||||
assert resolve_vin_year("1G1YE2J32A5602473") == 2040
|
assert resolve_vin_year("1G1YE2132A5602473") == 2040
|
||||||
|
|
||||||
@patch("app.engines.gemini_engine.datetime")
|
@patch("app.engines.gemini_engine.datetime")
|
||||||
def test_l_with_alpha_pos7_returns_2050_when_plausible(self, mock_dt):
|
def test_l_with_numeric_pos7_returns_2050_when_plausible(self, mock_dt):
|
||||||
"""L=2050 when position 7 is alphabetic and year 2050 is plausible."""
|
"""L=2050 when position 7 is numeric and year 2050 is plausible."""
|
||||||
mock_dt.now.return_value = datetime(2049, 6, 1)
|
mock_dt.now.return_value = datetime(2049, 6, 1)
|
||||||
assert resolve_vin_year("1G1YE2J32L5602473") == 2050
|
assert resolve_vin_year("1G1YE2132L5602473") == 2050
|
||||||
|
|
||||||
@patch("app.engines.gemini_engine.datetime")
|
@patch("app.engines.gemini_engine.datetime")
|
||||||
def test_a_with_alpha_pos7_returns_1980_when_2040_not_plausible(self, mock_dt):
|
def test_a_with_numeric_pos7_returns_1980_when_2040_not_plausible(self, mock_dt):
|
||||||
"""A=1980 when third cycle year (2040) exceeds max plausible."""
|
"""A=1980 when third cycle year (2040) exceeds max plausible."""
|
||||||
mock_dt.now.return_value = datetime(2026, 2, 20)
|
mock_dt.now.return_value = datetime(2026, 2, 20)
|
||||||
# 2026 + 2 = 2028 < 2040, so third cycle not plausible -> first cycle
|
# 2026 + 2 = 2028 < 2040, so third cycle not plausible -> first cycle
|
||||||
assert resolve_vin_year("1G1YE2J32A5602473") == 1980
|
assert resolve_vin_year("1G1YE2132A5602473") == 1980
|
||||||
|
|
||||||
@patch("app.engines.gemini_engine.datetime")
|
@patch("app.engines.gemini_engine.datetime")
|
||||||
def test_k_with_alpha_pos7_returns_2049_when_plausible(self, mock_dt):
|
def test_k_with_numeric_pos7_returns_2049_when_plausible(self, mock_dt):
|
||||||
"""K=2049 when position 7 is alphabetic and year is plausible."""
|
"""K=2049 when position 7 is numeric and year is plausible."""
|
||||||
mock_dt.now.return_value = datetime(2048, 1, 1)
|
mock_dt.now.return_value = datetime(2048, 1, 1)
|
||||||
assert resolve_vin_year("1G1YE2J32K5602473") == 2049
|
assert resolve_vin_year("1G1YE2132K5602473") == 2049
|
||||||
|
|
||||||
|
|
||||||
class TestEdgeCases:
|
class TestEdgeCases:
|
||||||
|
|||||||
Reference in New Issue
Block a user