mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:41:09 +02:00
feat(shared-types): add Zod schemas for AI structured outputs
Introduces packages/shared-types/src/ai-schemas.ts as the single source
of truth for the wire format between mana-api and the unified Mana app.
Two schemas:
- MealAnalysisSchema (foods, totalNutrition, description, confidence,
warnings, suggestions) — consumed by nutriphi /analysis/photo and
/analysis/text routes
- PlantIdentificationSchema (scientificName, commonNames, confidence,
health/watering/light advice, generalTips) — consumed by planta
/analysis/identify
Both schemas include .describe() annotations on every field. The Vercel
AI SDK passes these through to the model as part of the structured-output
prompt, which materially improves accuracy on Gemini Vision (the model
sees both the field name AND the German-language hint about what to put
there).
Schemas use plain .optional() rather than .nullable() because
generateObject() guides the model with strict schema adherence — it
won't emit JSON null for missing fields, just omit them.
Deps wired up:
- apps/api: + ai@6, + @ai-sdk/openai-compatible@2, + @mana/shared-types
- apps/mana/apps/web: + zod (for z.infer of the shared schemas)
- packages/shared-types: + zod (for the schema definitions themselves)
All three on zod ^3.23 to stay in lockstep with the existing
apps/api zod usage.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
63e1ef8233
commit
c2a75bb8e1
6 changed files with 454 additions and 325 deletions
|
|
@ -12,10 +12,13 @@
|
|||
"db:push": "drizzle-kit push"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai-compatible": "^2.0.41",
|
||||
"@mana/media-client": "workspace:*",
|
||||
"@mana/shared-hono": "workspace:*",
|
||||
"@mana/shared-storage": "workspace:*",
|
||||
"@mana/shared-types": "workspace:^",
|
||||
"@mozilla/readability": "^0.5.0",
|
||||
"ai": "^6.0.154",
|
||||
"drizzle-orm": "^0.38.0",
|
||||
"hono": "^4.7.0",
|
||||
"jsdom": "^25.0.0",
|
||||
|
|
|
|||
|
|
@ -81,7 +81,8 @@
|
|||
"suncalc": "^1.9.0",
|
||||
"svelte-dnd-action": "^0.9.68",
|
||||
"svelte-i18n": "^4.0.0",
|
||||
"svelte-sonner": "^1.0.5"
|
||||
"svelte-sonner": "^1.0.5",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,5 +15,8 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "^3.25.76"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
71
packages/shared-types/src/ai-schemas.ts
Normal file
71
packages/shared-types/src/ai-schemas.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
/**
|
||||
* Shared Zod schemas for AI structured-output endpoints.
|
||||
*
|
||||
* Single source of truth for the wire format between mana-api and the
|
||||
* unified Mana app. Backend routes use these schemas with the Vercel
|
||||
* AI SDK's `generateObject` to enforce a strict shape on Gemini Vision
|
||||
* responses; the frontend imports the inferred TypeScript types via
|
||||
* `z.infer<typeof Schema>` so changes here propagate end-to-end.
|
||||
*
|
||||
* Why a Zod schema instead of a plain TS interface?
|
||||
* - Runtime validation: if Gemini hallucinates a field, we throw
|
||||
* before the data hits the IndexedDB write path
|
||||
* - Provider-portable structured outputs: the AI SDK translates the
|
||||
* same schema into OpenAI strict json_schema, Anthropic tool-use,
|
||||
* or Gemini response_schema depending on which backend mana-llm
|
||||
* routes to
|
||||
* - Single source of truth: backend and frontend share one definition,
|
||||
* no manual type-drift between Hono routes and Svelte stores
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
// ─── NutriPhi: meal photo / text analysis ────────────────────────
|
||||
|
||||
const AnalyzedFoodSchema = z.object({
|
||||
name: z.string().describe('The food item name in German'),
|
||||
quantity: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Approximate portion description, e.g. "1 Tasse" or "200g"'),
|
||||
calories: z.number().optional().describe('Estimated calories for this item alone'),
|
||||
});
|
||||
|
||||
const NutritionDataSchema = z.object({
|
||||
calories: z.number().describe('Total kcal for the entire meal'),
|
||||
protein: z.number().describe('Protein in grams'),
|
||||
carbohydrates: z.number().describe('Carbohydrates in grams'),
|
||||
fat: z.number().describe('Fat in grams'),
|
||||
fiber: z.number().describe('Dietary fiber in grams'),
|
||||
sugar: z.number().describe('Sugar in grams'),
|
||||
});
|
||||
|
||||
export const MealAnalysisSchema = z.object({
|
||||
foods: z.array(AnalyzedFoodSchema).describe('Each individual food item the model could identify'),
|
||||
totalNutrition: NutritionDataSchema.describe('Sum across all foods'),
|
||||
description: z.string().describe('A short, human-readable summary of the meal in German'),
|
||||
confidence: z.number().min(0).max(1).describe('How confident the model is in its analysis, 0..1'),
|
||||
warnings: z
|
||||
.array(z.string())
|
||||
.default([])
|
||||
.describe('Optional health-related warnings (e.g. "high in sugar")'),
|
||||
suggestions: z.array(z.string()).default([]).describe('Optional suggestions to improve the meal'),
|
||||
});
|
||||
|
||||
export type MealAnalysis = z.infer<typeof MealAnalysisSchema>;
|
||||
export type AnalyzedFood = z.infer<typeof AnalyzedFoodSchema>;
|
||||
export type NutritionData = z.infer<typeof NutritionDataSchema>;
|
||||
|
||||
// ─── Planta: plant photo identification ──────────────────────────
|
||||
|
||||
export const PlantIdentificationSchema = z.object({
|
||||
scientificName: z.string().optional().describe('Latin binomial, e.g. "Monstera deliciosa"'),
|
||||
commonNames: z.array(z.string()).default([]).describe('Common names in German if known'),
|
||||
confidence: z.number().min(0).max(1).optional().describe('Identification confidence, 0..1'),
|
||||
healthAssessment: z.string().optional().describe('Brief health observation visible in the photo'),
|
||||
wateringAdvice: z.string().optional().describe('How often to water this species'),
|
||||
lightAdvice: z.string().optional().describe('Preferred light conditions'),
|
||||
generalTips: z.array(z.string()).default([]).describe('Other care tips, in German'),
|
||||
});
|
||||
|
||||
export type PlantIdentification = z.infer<typeof PlantIdentificationSchema>;
|
||||
|
|
@ -22,6 +22,9 @@ export * from './contact';
|
|||
// Landing page configuration types
|
||||
export * from './landing-config';
|
||||
|
||||
// AI structured-output Zod schemas (shared between mana-api + web frontend)
|
||||
export * from './ai-schemas';
|
||||
|
||||
// API types
|
||||
export interface User {
|
||||
id: string;
|
||||
|
|
|
|||
696
pnpm-lock.yaml
generated
696
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue