Compare commits
5 Commits
d96736789e
...
issue-223-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b5b84729f | ||
|
|
781241966c | ||
|
|
bf6742f6ea | ||
|
|
5bb44be8bc | ||
|
|
361f58d7c6 |
@@ -648,11 +648,12 @@ export class VehiclesService {
|
|||||||
engine_type = EXCLUDED.engine_type,
|
engine_type = EXCLUDED.engine_type,
|
||||||
body_type = EXCLUDED.body_type,
|
body_type = EXCLUDED.body_type,
|
||||||
raw_data = EXCLUDED.raw_data,
|
raw_data = EXCLUDED.raw_data,
|
||||||
cached_at = NOW()`,
|
cached_at = NOW()
|
||||||
[vin, response.make, response.model, response.year, response.engine, response.bodyType, JSON.stringify(response)]
|
WHERE (vin_cache.raw_data->>'confidence')::float <= $8`,
|
||||||
|
[vin, response.make, response.model, response.year, response.engine, response.bodyType, JSON.stringify(response), response.confidence ?? 1]
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.debug('VIN cached', { vin });
|
logger.debug('VIN cached', { vin, confidence: response.confidence });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to cache VIN data', { vin, error });
|
logger.error('Failed to cache VIN data', { vin, error });
|
||||||
// Don't throw - caching failure shouldn't break the decode flow
|
// Don't throw - caching failure shouldn't break the decode flow
|
||||||
@@ -741,6 +742,12 @@ export class VehiclesService {
|
|||||||
const sourceEngine = response.engine;
|
const sourceEngine = response.engine;
|
||||||
const sourceTransmission = response.transmission;
|
const sourceTransmission = response.transmission;
|
||||||
|
|
||||||
|
logger.debug('VIN decode raw values', {
|
||||||
|
vin: response.vin,
|
||||||
|
year: sourceYear, make: sourceMake, model: sourceModel,
|
||||||
|
trim: sourceTrim, confidence: response.confidence
|
||||||
|
});
|
||||||
|
|
||||||
// Year is always high confidence if present (exact numeric match)
|
// Year is always high confidence if present (exact numeric match)
|
||||||
const year: MatchedField<number> = {
|
const year: MatchedField<number> = {
|
||||||
value: sourceYear,
|
value: sourceYear,
|
||||||
@@ -854,6 +861,26 @@ export class VehiclesService {
|
|||||||
return { value: containsMatch, sourceValue, confidence: 'medium' };
|
return { value: containsMatch, sourceValue, confidence: 'medium' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try reverse contains - source value contains option (e.g., source "X5 xDrive35i" contains option "X5")
|
||||||
|
// Prefer the longest matching option to avoid false positives (e.g., "X5 M" over "X5")
|
||||||
|
const reverseMatches = options.filter(opt => {
|
||||||
|
const normalizedOpt = opt.toLowerCase().trim();
|
||||||
|
return normalizedSource.includes(normalizedOpt) && normalizedOpt.length > 0;
|
||||||
|
});
|
||||||
|
if (reverseMatches.length > 0) {
|
||||||
|
const bestMatch = reverseMatches.reduce((a, b) => a.length >= b.length ? a : b);
|
||||||
|
return { value: bestMatch, sourceValue, confidence: 'medium' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try word-start match - source starts with option + separator (e.g., "X5 xDrive" starts with "X5 ")
|
||||||
|
const wordStartMatch = options.find(opt => {
|
||||||
|
const normalizedOpt = opt.toLowerCase().trim();
|
||||||
|
return normalizedSource.startsWith(normalizedOpt + ' ') || normalizedSource.startsWith(normalizedOpt + '-');
|
||||||
|
});
|
||||||
|
if (wordStartMatch) {
|
||||||
|
return { value: wordStartMatch, sourceValue, confidence: 'medium' };
|
||||||
|
}
|
||||||
|
|
||||||
// No match found - return source value as hint with no match
|
// No match found - return source value as hint with no match
|
||||||
return { value: null, sourceValue, confidence: 'none' };
|
return { value: null, sourceValue, confidence: 'none' };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -206,8 +206,8 @@ services:
|
|||||||
VISION_MONTHLY_LIMIT: "1000"
|
VISION_MONTHLY_LIMIT: "1000"
|
||||||
# Vertex AI / Gemini configuration (maintenance schedule extraction)
|
# Vertex AI / Gemini configuration (maintenance schedule extraction)
|
||||||
VERTEX_AI_PROJECT: motovaultpro
|
VERTEX_AI_PROJECT: motovaultpro
|
||||||
VERTEX_AI_LOCATION: us-central1
|
VERTEX_AI_LOCATION: global
|
||||||
GEMINI_MODEL: gemini-2.5-flash
|
GEMINI_MODEL: gemini-3-flash-preview
|
||||||
volumes:
|
volumes:
|
||||||
- /tmp/vin-debug:/tmp/vin-debug
|
- /tmp/vin-debug:/tmp/vin-debug
|
||||||
- ./secrets/app/auth0-ocr-client-id.txt:/run/secrets/auth0-ocr-client-id:ro
|
- ./secrets/app/auth0-ocr-client-id.txt:/run/secrets/auth0-ocr-client-id:ro
|
||||||
|
|||||||
@@ -86,7 +86,9 @@ export const vehiclesApi = {
|
|||||||
* Requires Pro or Enterprise tier
|
* Requires Pro or Enterprise tier
|
||||||
*/
|
*/
|
||||||
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
|
||||||
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
|
|||||||
const [isDecoding, setIsDecoding] = useState(false);
|
const [isDecoding, setIsDecoding] = useState(false);
|
||||||
const [showUpgradeDialog, setShowUpgradeDialog] = useState(false);
|
const [showUpgradeDialog, setShowUpgradeDialog] = useState(false);
|
||||||
const [decodeError, setDecodeError] = useState<string | null>(null);
|
const [decodeError, setDecodeError] = useState<string | null>(null);
|
||||||
|
const [decodeHint, setDecodeHint] = useState<string | null>(null);
|
||||||
|
|
||||||
// VIN OCR capture hook
|
// VIN OCR capture hook
|
||||||
const vinOcr = useVinOcr();
|
const vinOcr = useVinOcr();
|
||||||
@@ -524,6 +525,7 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
|
|||||||
|
|
||||||
setIsDecoding(true);
|
setIsDecoding(true);
|
||||||
setDecodeError(null);
|
setDecodeError(null);
|
||||||
|
setDecodeHint(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const decoded = await vehiclesApi.decodeVin(vin);
|
const decoded = await vehiclesApi.decodeVin(vin);
|
||||||
@@ -588,6 +590,21 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
|
|||||||
setValue('transmission', decoded.transmission.value);
|
setValue('transmission', decoded.transmission.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if decode returned data but matching failed for key fields
|
||||||
|
const hasMatchedValue = decoded.year.value || decoded.make.value || decoded.model.value;
|
||||||
|
const hasSourceValue = decoded.year.sourceValue || decoded.make.sourceValue || decoded.model.sourceValue;
|
||||||
|
if (!hasMatchedValue && hasSourceValue) {
|
||||||
|
const parts = [
|
||||||
|
decoded.year.sourceValue,
|
||||||
|
decoded.make.sourceValue,
|
||||||
|
decoded.model.sourceValue,
|
||||||
|
decoded.trimLevel.sourceValue
|
||||||
|
].filter(Boolean);
|
||||||
|
setDecodeHint(
|
||||||
|
`Could not match VIN data to dropdowns. Decoded as: ${parts.join(' ')}. Please select values manually.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
setLoadingDropdowns(false);
|
setLoadingDropdowns(false);
|
||||||
isVinDecoding.current = false;
|
isVinDecoding.current = false;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -671,6 +688,9 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
|
|||||||
{decodeError && (
|
{decodeError && (
|
||||||
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{decodeError}</p>
|
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{decodeError}</p>
|
||||||
)}
|
)}
|
||||||
|
{decodeHint && (
|
||||||
|
<p className="mt-1 text-sm text-amber-600 dark:text-amber-400">{decodeHint}</p>
|
||||||
|
)}
|
||||||
{vinOcr.error && (
|
{vinOcr.error && (
|
||||||
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{vinOcr.error}</p>
|
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{vinOcr.error}</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class Settings:
|
|||||||
# Vertex AI / Gemini configuration
|
# Vertex AI / Gemini configuration
|
||||||
self.vertex_ai_project: str = os.getenv("VERTEX_AI_PROJECT", "")
|
self.vertex_ai_project: str = os.getenv("VERTEX_AI_PROJECT", "")
|
||||||
self.vertex_ai_location: str = os.getenv(
|
self.vertex_ai_location: str = os.getenv(
|
||||||
"VERTEX_AI_LOCATION", "us-central1"
|
"VERTEX_AI_LOCATION", "global"
|
||||||
)
|
)
|
||||||
self.gemini_model: str = os.getenv("GEMINI_MODEL", "gemini-2.5-flash")
|
self.gemini_model: str = os.getenv("GEMINI_MODEL", "gemini-2.5-flash")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user