server.go 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908
  1. package howtocook
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "math"
  7. "math/rand"
  8. "strings"
  9. "github.com/bytedance/sonic"
  10. mcpservers "github.com/labring/aiproxy/mcp-servers"
  11. "github.com/mark3labs/mcp-go/mcp"
  12. "github.com/mark3labs/mcp-go/server"
  13. )
  14. const (
  15. RecipesURL = "https://weilei.site/all_recipes.json"
  16. Version = "0.0.6"
  17. )
  18. // Server represents the HowToCook MCP server
  19. type Server struct {
  20. *server.MCPServer
  21. recipes []Recipe
  22. categories []string
  23. }
  24. // NewServer creates a new HowToCook MCP server
  25. func NewServer(_, _ map[string]string) (mcpservers.Server, error) {
  26. // Create MCP server
  27. mcpServer := server.NewMCPServer("howtocook-mcp", Version)
  28. cookServer := &Server{
  29. MCPServer: mcpServer,
  30. }
  31. // Initialize recipes and categories
  32. if err := cookServer.initialize(context.Background()); err != nil {
  33. return nil, fmt.Errorf("failed to initialize server: %w", err)
  34. }
  35. // Add tools
  36. cookServer.addTools()
  37. return cookServer, nil
  38. }
  39. func ListTools(ctx context.Context) ([]mcp.Tool, error) {
  40. cookServer := &Server{
  41. MCPServer: server.NewMCPServer("howtocook-mcp", Version),
  42. }
  43. cookServer.addTools()
  44. return mcpservers.ListServerTools(ctx, cookServer)
  45. }
  46. // initialize loads recipe data and categories
  47. func (s *Server) initialize(ctx context.Context) error {
  48. recipes, err := s.fetchRecipes(ctx)
  49. if err != nil {
  50. return fmt.Errorf("failed to fetch recipes: %w", err)
  51. }
  52. if len(recipes) == 0 {
  53. return errors.New("no recipes found")
  54. }
  55. s.recipes = recipes
  56. s.categories = s.getAllCategories()
  57. return nil
  58. }
  59. // addTools adds all tools to the server
  60. func (s *Server) addTools() {
  61. s.addGetAllRecipesTool()
  62. s.addGetRecipesByCategoryTool()
  63. s.addRecommendMealsTool()
  64. s.addWhatToEatTool()
  65. }
  66. // addGetAllRecipesTool adds the get all recipes tool
  67. func (s *Server) addGetAllRecipesTool() {
  68. s.AddTool(
  69. mcp.Tool{
  70. Name: "mcp_howtocook_getAllRecipes",
  71. Description: "获取所有菜谱",
  72. InputSchema: mcp.ToolInputSchema{
  73. Type: "object",
  74. Properties: map[string]any{},
  75. Required: []string{},
  76. },
  77. },
  78. func(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
  79. simplifiedRecipes := make([]NameOnlyRecipe, 0, len(s.recipes))
  80. for _, recipe := range s.recipes {
  81. simplifiedRecipes = append(simplifiedRecipes, NameOnlyRecipe{
  82. Name: recipe.Name,
  83. Description: recipe.Description,
  84. })
  85. }
  86. result, err := sonic.Marshal(simplifiedRecipes)
  87. if err != nil {
  88. return nil, fmt.Errorf("failed to marshal recipes: %w", err)
  89. }
  90. return mcp.NewToolResultText(string(result)), nil
  91. },
  92. )
  93. }
  94. // addGetRecipesByCategoryTool adds the get recipes by category tool
  95. func (s *Server) addGetRecipesByCategoryTool() {
  96. s.AddTool(
  97. mcp.Tool{
  98. Name: "mcp_howtocook_getRecipesByCategory",
  99. Description: "根据分类查询菜谱,可选分类有: " + strings.Join(s.categories, ", "),
  100. InputSchema: mcp.ToolInputSchema{
  101. Type: "object",
  102. Properties: map[string]any{
  103. "category": map[string]any{
  104. "type": "string",
  105. "description": "菜谱分类名称,如水产、早餐、荤菜、主食等",
  106. "enum": s.categories,
  107. },
  108. },
  109. Required: []string{"category"},
  110. },
  111. },
  112. s.handleGetRecipesByCategory,
  113. )
  114. }
  115. // addRecommendMealsTool adds the recommend meals tool
  116. func (s *Server) addRecommendMealsTool() {
  117. s.AddTool(
  118. mcp.Tool{
  119. Name: "mcp_howtocook_recommendMeals",
  120. Description: "根据用户的忌口、过敏原、人数智能推荐菜谱,创建一周的膳食计划以及大致的购物清单",
  121. InputSchema: mcp.ToolInputSchema{
  122. Type: "object",
  123. Properties: map[string]any{
  124. "allergies": map[string]any{
  125. "type": "array",
  126. "items": map[string]any{"type": "string"},
  127. "description": "过敏原列表,如[\"大蒜\", \"虾\"]",
  128. },
  129. "avoidItems": map[string]any{
  130. "type": "array",
  131. "items": map[string]any{"type": "string"},
  132. "description": "忌口食材列表,如[\"葱\", \"姜\"]",
  133. },
  134. "peopleCount": map[string]any{
  135. "type": "integer",
  136. "minimum": 1,
  137. "maximum": 10,
  138. "description": "用餐人数,1-10之间的整数",
  139. },
  140. },
  141. Required: []string{"peopleCount"},
  142. },
  143. },
  144. s.handleRecommendMeals,
  145. )
  146. }
  147. // addWhatToEatTool adds the what to eat tool
  148. func (s *Server) addWhatToEatTool() {
  149. s.AddTool(
  150. mcp.Tool{
  151. Name: "mcp_howtocook_whatToEat",
  152. Description: "不知道吃什么?根据人数直接推荐适合的菜品组合",
  153. InputSchema: mcp.ToolInputSchema{
  154. Type: "object",
  155. Properties: map[string]any{
  156. "peopleCount": map[string]any{
  157. "type": "integer",
  158. "minimum": 1,
  159. "maximum": 10,
  160. "description": "用餐人数,1-10之间的整数,会根据人数推荐合适数量的菜品",
  161. },
  162. },
  163. Required: []string{"peopleCount"},
  164. },
  165. },
  166. s.handleWhatToEat,
  167. )
  168. }
  169. // handleGetRecipesByCategory handles the get recipes by category request
  170. func (s *Server) handleGetRecipesByCategory(
  171. _ context.Context,
  172. request mcp.CallToolRequest,
  173. ) (*mcp.CallToolResult, error) {
  174. args := request.GetArguments()
  175. category, ok := args["category"].(string)
  176. if !ok || category == "" {
  177. return nil, errors.New("category is required")
  178. }
  179. var filteredRecipes []SimpleRecipe
  180. for _, recipe := range s.recipes {
  181. if recipe.Category == category {
  182. filteredRecipes = append(filteredRecipes, s.simplifyRecipe(recipe))
  183. }
  184. }
  185. result, err := sonic.Marshal(filteredRecipes)
  186. if err != nil {
  187. return nil, fmt.Errorf("failed to marshal recipes: %w", err)
  188. }
  189. return mcp.NewToolResultText(string(result)), nil
  190. }
  191. // handleRecommendMeals handles the recommend meals request
  192. func (s *Server) handleRecommendMeals(
  193. _ context.Context,
  194. request mcp.CallToolRequest,
  195. ) (*mcp.CallToolResult, error) {
  196. args := request.GetArguments()
  197. peopleCount, ok := args["peopleCount"].(float64)
  198. if !ok {
  199. return nil, errors.New("peopleCount is required")
  200. }
  201. var allergies, avoidItems []string
  202. if allergiesRaw, ok := args["allergies"].([]any); ok {
  203. for _, allergy := range allergiesRaw {
  204. if allergyStr, ok := allergy.(string); ok {
  205. allergies = append(allergies, allergyStr)
  206. }
  207. }
  208. }
  209. if avoidItemsRaw, ok := args["avoidItems"].([]any); ok {
  210. for _, item := range avoidItemsRaw {
  211. if itemStr, ok := item.(string); ok {
  212. avoidItems = append(avoidItems, itemStr)
  213. }
  214. }
  215. }
  216. mealPlan := s.generateMealPlan(int(peopleCount), allergies, avoidItems)
  217. result, err := sonic.Marshal(mealPlan)
  218. if err != nil {
  219. return nil, fmt.Errorf("failed to marshal meal plan: %w", err)
  220. }
  221. return mcp.NewToolResultText(string(result)), nil
  222. }
  223. // handleWhatToEat handles the what to eat request
  224. func (s *Server) handleWhatToEat(
  225. _ context.Context,
  226. request mcp.CallToolRequest,
  227. ) (*mcp.CallToolResult, error) {
  228. args := request.GetArguments()
  229. peopleCount, ok := args["peopleCount"].(float64)
  230. if !ok {
  231. return nil, errors.New("peopleCount is required")
  232. }
  233. recommendation := s.generateDishRecommendation(int(peopleCount))
  234. result, err := sonic.Marshal(recommendation)
  235. if err != nil {
  236. return nil, fmt.Errorf("failed to marshal recommendation: %w", err)
  237. }
  238. return mcp.NewToolResultText(string(result)), nil
  239. }
  240. // generateMealPlan generates a weekly meal plan
  241. func (s *Server) generateMealPlan(peopleCount int, allergies, avoidItems []string) MealPlan {
  242. // Filter recipes based on allergies and avoid items
  243. filteredRecipes := s.filterRecipes(allergies, avoidItems)
  244. // Group recipes by category
  245. recipesByCategory := s.groupRecipesByCategory(filteredRecipes)
  246. mealPlan := MealPlan{
  247. Weekdays: make([]DayPlan, 5),
  248. Weekend: make([]DayPlan, 2),
  249. GroceryList: GroceryList{
  250. Ingredients: []GroceryItem{},
  251. ShoppingPlan: ShoppingPlan{
  252. Fresh: []string{},
  253. Pantry: []string{},
  254. Spices: []string{},
  255. Others: []string{},
  256. },
  257. },
  258. }
  259. var selectedRecipes []Recipe
  260. // Generate weekday plans
  261. weekdays := []string{"周一", "周二", "周三", "周四", "周五"}
  262. for i := range 5 {
  263. dayPlan := DayPlan{
  264. Day: weekdays[i],
  265. Breakfast: []SimpleRecipe{},
  266. Lunch: []SimpleRecipe{},
  267. Dinner: []SimpleRecipe{},
  268. }
  269. // Breakfast
  270. breakfastCount := int(math.Max(1, math.Ceil(float64(peopleCount)/5)))
  271. dayPlan.Breakfast, selectedRecipes = s.selectMeals(
  272. recipesByCategory,
  273. "早餐",
  274. breakfastCount,
  275. selectedRecipes,
  276. )
  277. // Lunch and dinner
  278. mealCount := int(math.Max(2, math.Ceil(float64(peopleCount)/3)))
  279. dayPlan.Lunch, selectedRecipes = s.selectVariedMeals(
  280. recipesByCategory,
  281. mealCount,
  282. selectedRecipes,
  283. )
  284. dayPlan.Dinner, selectedRecipes = s.selectVariedMeals(
  285. recipesByCategory,
  286. mealCount,
  287. selectedRecipes,
  288. )
  289. mealPlan.Weekdays[i] = dayPlan
  290. }
  291. // Generate weekend plans
  292. weekendDays := []string{"周六", "周日"}
  293. for i := range 2 {
  294. dayPlan := DayPlan{
  295. Day: weekendDays[i],
  296. Breakfast: []SimpleRecipe{},
  297. Lunch: []SimpleRecipe{},
  298. Dinner: []SimpleRecipe{},
  299. }
  300. // Weekend breakfast
  301. breakfastCount := int(math.Max(2, math.Ceil(float64(peopleCount)/3)))
  302. dayPlan.Breakfast, selectedRecipes = s.selectMeals(
  303. recipesByCategory,
  304. "早餐",
  305. breakfastCount,
  306. selectedRecipes,
  307. )
  308. // Weekend meals (more dishes)
  309. weekdayMealCount := int(math.Max(2, math.Ceil(float64(peopleCount)/3)))
  310. var weekendAddition int
  311. if peopleCount <= 4 {
  312. weekendAddition = 1
  313. } else {
  314. weekendAddition = 2
  315. }
  316. mealCount := weekdayMealCount + weekendAddition
  317. dayPlan.Lunch, selectedRecipes = s.selectWeekendMeals(
  318. recipesByCategory,
  319. mealCount,
  320. selectedRecipes,
  321. )
  322. dayPlan.Dinner, selectedRecipes = s.selectWeekendMeals(
  323. recipesByCategory,
  324. mealCount,
  325. selectedRecipes,
  326. )
  327. mealPlan.Weekend[i] = dayPlan
  328. }
  329. // Generate grocery list
  330. mealPlan.GroceryList = s.generateGroceryList(selectedRecipes)
  331. return mealPlan
  332. }
  333. // generateDishRecommendation generates dish recommendations based on people count
  334. func (s *Server) generateDishRecommendation(peopleCount int) DishRecommendation {
  335. vegetableCount := (peopleCount + 1) / 2
  336. meatCount := int(math.Ceil(float64(peopleCount+1) / 2))
  337. var meatDishes []Recipe
  338. for _, recipe := range s.recipes {
  339. if recipe.Category == "荤菜" || recipe.Category == "水产" {
  340. meatDishes = append(meatDishes, recipe)
  341. }
  342. }
  343. var vegetableDishes []Recipe
  344. for _, recipe := range s.recipes {
  345. if recipe.Category != "荤菜" && recipe.Category != "水产" &&
  346. recipe.Category != "早餐" && recipe.Category != "主食" {
  347. vegetableDishes = append(vegetableDishes, recipe)
  348. }
  349. }
  350. var (
  351. recommendedDishes []Recipe
  352. fishDish *Recipe
  353. )
  354. // Add fish dish for large groups
  355. if peopleCount > 8 {
  356. var fishDishes []Recipe
  357. for _, recipe := range s.recipes {
  358. if recipe.Category == "水产" {
  359. fishDishes = append(fishDishes, recipe)
  360. }
  361. }
  362. if len(fishDishes) > 0 {
  363. selected := fishDishes[rand.Intn(len(fishDishes))]
  364. fishDish = &selected
  365. recommendedDishes = append(recommendedDishes, selected)
  366. }
  367. }
  368. // Select meat dishes
  369. remainingMeatCount := meatCount
  370. if fishDish != nil {
  371. remainingMeatCount--
  372. }
  373. selectedMeatDishes := s.selectMeatDishes(meatDishes, remainingMeatCount)
  374. recommendedDishes = append(recommendedDishes, selectedMeatDishes...)
  375. // Select vegetable dishes
  376. selectedVegetableDishes := s.selectRandomDishes(vegetableDishes, vegetableCount)
  377. recommendedDishes = append(recommendedDishes, selectedVegetableDishes...)
  378. // Convert to simple recipes
  379. simpleDishes := make([]SimpleRecipe, len(recommendedDishes))
  380. for _, dish := range recommendedDishes {
  381. simpleDishes = append(simpleDishes, s.simplifyRecipe(dish))
  382. }
  383. fishCount := 0
  384. if fishDish != nil {
  385. fishCount = 1
  386. }
  387. return DishRecommendation{
  388. PeopleCount: peopleCount,
  389. MeatDishCount: len(selectedMeatDishes) + fishCount,
  390. VegetableDishCount: len(selectedVegetableDishes),
  391. Dishes: simpleDishes,
  392. Message: fmt.Sprintf("为%d人推荐的菜品,包含%d个荤菜和%d个素菜。",
  393. peopleCount, len(selectedMeatDishes)+fishCount, len(selectedVegetableDishes)),
  394. }
  395. }
  396. // Helper methods would continue here...
  397. // Due to length constraints, I'll provide the key helper methods:
  398. // simplifyRecipe converts Recipe to SimpleRecipe
  399. func (s *Server) simplifyRecipe(recipe Recipe) SimpleRecipe {
  400. ingredients := make([]SimpleIngredient, len(recipe.Ingredients))
  401. for _, ing := range recipe.Ingredients {
  402. ingredients = append(ingredients, SimpleIngredient{
  403. Name: ing.Name,
  404. TextQuantity: ing.TextQuantity,
  405. })
  406. }
  407. return SimpleRecipe{
  408. ID: recipe.ID,
  409. Name: recipe.Name,
  410. Description: recipe.Description,
  411. Ingredients: ingredients,
  412. }
  413. }
  414. // filterRecipes filters recipes based on allergies and avoid items
  415. func (s *Server) filterRecipes(allergies, avoidItems []string) []Recipe {
  416. var filtered []Recipe
  417. for _, recipe := range s.recipes {
  418. hasAllergyOrAvoid := false
  419. for _, ingredient := range recipe.Ingredients {
  420. name := strings.ToLower(ingredient.Name)
  421. for _, allergy := range allergies {
  422. if strings.Contains(name, strings.ToLower(allergy)) {
  423. hasAllergyOrAvoid = true
  424. break
  425. }
  426. }
  427. if hasAllergyOrAvoid {
  428. break
  429. }
  430. for _, avoid := range avoidItems {
  431. if strings.Contains(name, strings.ToLower(avoid)) {
  432. hasAllergyOrAvoid = true
  433. break
  434. }
  435. }
  436. if hasAllergyOrAvoid {
  437. break
  438. }
  439. }
  440. if !hasAllergyOrAvoid {
  441. filtered = append(filtered, recipe)
  442. }
  443. }
  444. return filtered
  445. }
  446. func (s *Server) groupRecipesByCategory(recipes []Recipe) map[string][]Recipe {
  447. recipesByCategory := make(map[string][]Recipe)
  448. targetCategories := []string{"水产", "早餐", "荤菜", "主食", "素菜", "甜品", "汤羹"}
  449. for _, recipe := range recipes {
  450. for _, category := range targetCategories {
  451. if recipe.Category == category {
  452. if recipesByCategory[category] == nil {
  453. recipesByCategory[category] = []Recipe{}
  454. }
  455. recipesByCategory[category] = append(recipesByCategory[category], recipe)
  456. break
  457. }
  458. }
  459. }
  460. return recipesByCategory
  461. }
  462. // selectMeals selects meals from a specific category
  463. func (s *Server) selectMeals(
  464. recipesByCategory map[string][]Recipe,
  465. category string,
  466. count int,
  467. selectedRecipes []Recipe,
  468. ) ([]SimpleRecipe, []Recipe) {
  469. var meals []SimpleRecipe
  470. if recipes, exists := recipesByCategory[category]; exists && len(recipes) > 0 {
  471. for i := 0; i < count && len(recipes) > 0; i++ {
  472. index := rand.Intn(len(recipes))
  473. selectedRecipe := recipes[index]
  474. selectedRecipes = append(selectedRecipes, selectedRecipe)
  475. meals = append(meals, s.simplifyRecipe(selectedRecipe))
  476. // Remove selected recipe to avoid duplication
  477. recipes = append(recipes[:index], recipes[index+1:]...)
  478. recipesByCategory[category] = recipes
  479. }
  480. }
  481. return meals, selectedRecipes
  482. }
  483. // selectVariedMeals selects meals from various categories for lunch/dinner
  484. func (s *Server) selectVariedMeals(
  485. recipesByCategory map[string][]Recipe,
  486. count int,
  487. selectedRecipes []Recipe,
  488. ) ([]SimpleRecipe, []Recipe) {
  489. var meals []SimpleRecipe
  490. categories := []string{"主食", "水产", "荤菜", "素菜", "甜品"}
  491. for range count {
  492. selectedCategory := categories[rand.Intn(len(categories))]
  493. // Try to find a category with available recipes
  494. attempts := 0
  495. for attempts < len(categories) {
  496. if recipes, exists := recipesByCategory[selectedCategory]; exists && len(recipes) > 0 {
  497. index := rand.Intn(len(recipes))
  498. selectedRecipe := recipes[index]
  499. selectedRecipes = append(selectedRecipes, selectedRecipe)
  500. meals = append(meals, s.simplifyRecipe(selectedRecipe))
  501. // Remove selected recipe
  502. recipes = append(recipes[:index], recipes[index+1:]...)
  503. recipesByCategory[selectedCategory] = recipes
  504. break
  505. }
  506. // Try next category
  507. attempts++
  508. selectedCategory = categories[(rand.Intn(len(categories)))]
  509. }
  510. }
  511. return meals, selectedRecipes
  512. }
  513. // selectWeekendMeals selects meals for weekend with preference for meat and seafood
  514. func (s *Server) selectWeekendMeals(
  515. recipesByCategory map[string][]Recipe,
  516. count int,
  517. selectedRecipes []Recipe,
  518. ) ([]SimpleRecipe, []Recipe) {
  519. var meals []SimpleRecipe
  520. categories := []string{"荤菜", "水产"}
  521. for i := range count {
  522. category := categories[i%len(categories)]
  523. if recipes, exists := recipesByCategory[category]; exists && len(recipes) > 0 {
  524. index := rand.Intn(len(recipes))
  525. selectedRecipe := recipes[index]
  526. selectedRecipes = append(selectedRecipes, selectedRecipe)
  527. meals = append(meals, s.simplifyRecipe(selectedRecipe))
  528. // Remove selected recipe
  529. recipes = append(recipes[:index], recipes[index+1:]...)
  530. recipesByCategory[category] = recipes
  531. } else if recipes, exists := recipesByCategory["主食"]; exists && len(recipes) > 0 {
  532. // Fallback to 主食 if no meat/seafood available
  533. index := rand.Intn(len(recipes))
  534. selectedRecipe := recipes[index]
  535. selectedRecipes = append(selectedRecipes, selectedRecipe)
  536. meals = append(meals, s.simplifyRecipe(selectedRecipe))
  537. // Remove selected recipe
  538. recipes = append(recipes[:index], recipes[index+1:]...)
  539. recipesByCategory["主食"] = recipes
  540. }
  541. }
  542. return meals, selectedRecipes
  543. }
  544. // selectMeatDishes selects meat dishes with preference for different meat types
  545. func (s *Server) selectMeatDishes(meatDishes []Recipe, count int) []Recipe {
  546. //nolint:prealloc
  547. var selectedMeatDishes []Recipe
  548. meatTypes := []string{"猪肉", "鸡肉", "牛肉", "羊肉", "鸭肉", "鱼肉"}
  549. availableDishes := make([]Recipe, len(meatDishes))
  550. copy(availableDishes, meatDishes)
  551. // Try to select different meat types
  552. for _, meatType := range meatTypes {
  553. if len(selectedMeatDishes) >= count {
  554. break
  555. }
  556. var (
  557. meatTypeOptions []Recipe
  558. meatTypeIndices []int
  559. )
  560. for i, dish := range availableDishes {
  561. for _, ingredient := range dish.Ingredients {
  562. if strings.Contains(strings.ToLower(ingredient.Name), strings.ToLower(meatType)) {
  563. meatTypeOptions = append(meatTypeOptions, dish)
  564. meatTypeIndices = append(meatTypeIndices, i)
  565. break
  566. }
  567. }
  568. }
  569. if len(meatTypeOptions) > 0 {
  570. selectedIndex := rand.Intn(len(meatTypeOptions))
  571. selectedMeatDishes = append(selectedMeatDishes, meatTypeOptions[selectedIndex])
  572. // Remove selected dish from available dishes
  573. originalIndex := meatTypeIndices[selectedIndex]
  574. availableDishes = append(
  575. availableDishes[:originalIndex],
  576. availableDishes[originalIndex+1:]...)
  577. // Adjust indices for remaining items
  578. for j := range meatTypeIndices {
  579. if meatTypeIndices[j] > originalIndex {
  580. meatTypeIndices[j]--
  581. }
  582. }
  583. }
  584. }
  585. // Fill remaining slots with random meat dishes
  586. for len(selectedMeatDishes) < count && len(availableDishes) > 0 {
  587. index := rand.Intn(len(availableDishes))
  588. selectedMeatDishes = append(selectedMeatDishes, availableDishes[index])
  589. availableDishes = append(availableDishes[:index], availableDishes[index+1:]...)
  590. }
  591. return selectedMeatDishes
  592. }
  593. // selectRandomDishes selects random dishes from a list
  594. func (s *Server) selectRandomDishes(dishes []Recipe, count int) []Recipe {
  595. //nolint:prealloc
  596. var selectedDishes []Recipe
  597. availableDishes := make([]Recipe, len(dishes))
  598. copy(availableDishes, dishes)
  599. for len(selectedDishes) < count && len(availableDishes) > 0 {
  600. index := rand.Intn(len(availableDishes))
  601. selectedDishes = append(selectedDishes, availableDishes[index])
  602. availableDishes = append(availableDishes[:index], availableDishes[index+1:]...)
  603. }
  604. return selectedDishes
  605. }
  606. // generateGroceryList generates a grocery list from selected recipes
  607. func (s *Server) generateGroceryList(selectedRecipes []Recipe) GroceryList {
  608. ingredientMap := make(map[string]*GroceryItem)
  609. // Process all recipes
  610. for _, recipe := range selectedRecipes {
  611. s.processRecipeIngredients(recipe, ingredientMap)
  612. }
  613. // Convert map to slice
  614. ingredients := make([]GroceryItem, len(ingredientMap))
  615. for _, item := range ingredientMap {
  616. ingredients = append(ingredients, *item)
  617. }
  618. // Sort by usage frequency
  619. for i := range len(ingredients) - 1 {
  620. for j := range len(ingredients) - 1 - i {
  621. if ingredients[j].RecipeCount < ingredients[j+1].RecipeCount {
  622. ingredients[j], ingredients[j+1] = ingredients[j+1], ingredients[j]
  623. }
  624. }
  625. }
  626. // Generate shopping plan
  627. shoppingPlan := ShoppingPlan{
  628. Fresh: []string{},
  629. Pantry: []string{},
  630. Spices: []string{},
  631. Others: []string{},
  632. }
  633. s.categorizeIngredients(ingredients, &shoppingPlan)
  634. return GroceryList{
  635. Ingredients: ingredients,
  636. ShoppingPlan: shoppingPlan,
  637. }
  638. }
  639. // processRecipeIngredients processes ingredients from a recipe
  640. func (s *Server) processRecipeIngredients(recipe Recipe, ingredientMap map[string]*GroceryItem) {
  641. for _, ingredient := range recipe.Ingredients {
  642. key := strings.ToLower(ingredient.Name)
  643. if existingItem, exists := ingredientMap[key]; exists {
  644. // Update existing ingredient
  645. if existingItem.Unit != nil && ingredient.Unit != nil &&
  646. *existingItem.Unit == *ingredient.Unit &&
  647. existingItem.TotalQuantity != nil && ingredient.Quantity != nil {
  648. *existingItem.TotalQuantity += *ingredient.Quantity
  649. } else {
  650. // Set to nil if units don't match or quantities are uncertain
  651. existingItem.TotalQuantity = nil
  652. existingItem.Unit = nil
  653. }
  654. existingItem.RecipeCount++
  655. // Add recipe name if not already present
  656. found := false
  657. for _, recipeName := range existingItem.Recipes {
  658. if recipeName == recipe.Name {
  659. found = true
  660. break
  661. }
  662. }
  663. if !found {
  664. existingItem.Recipes = append(existingItem.Recipes, recipe.Name)
  665. }
  666. } else {
  667. // Create new ingredient entry
  668. newItem := &GroceryItem{
  669. Name: ingredient.Name,
  670. TotalQuantity: ingredient.Quantity,
  671. Unit: ingredient.Unit,
  672. RecipeCount: 1,
  673. Recipes: []string{recipe.Name},
  674. }
  675. ingredientMap[key] = newItem
  676. }
  677. }
  678. }
  679. // categorizeIngredients categorizes ingredients into shopping plan categories
  680. func (s *Server) categorizeIngredients(ingredients []GroceryItem, shoppingPlan *ShoppingPlan) {
  681. spiceKeywords := []string{
  682. "盐",
  683. "糖",
  684. "酱油",
  685. "醋",
  686. "料酒",
  687. "香料",
  688. "胡椒",
  689. "孜然",
  690. "辣椒",
  691. "花椒",
  692. "姜",
  693. "蒜",
  694. "葱",
  695. "调味",
  696. }
  697. freshKeywords := []string{
  698. "肉",
  699. "鱼",
  700. "虾",
  701. "蛋",
  702. "奶",
  703. "菜",
  704. "菠菜",
  705. "白菜",
  706. "青菜",
  707. "豆腐",
  708. "生菜",
  709. "水产",
  710. "豆芽",
  711. "西红柿",
  712. "番茄",
  713. "水果",
  714. "香菇",
  715. "木耳",
  716. "蘑菇",
  717. }
  718. pantryKeywords := []string{
  719. "米",
  720. "面",
  721. "粉",
  722. "油",
  723. "酒",
  724. "醋",
  725. "糖",
  726. "盐",
  727. "酱",
  728. "豆",
  729. "干",
  730. "罐头",
  731. "方便面",
  732. "面条",
  733. "米饭",
  734. "意大利面",
  735. "燕麦",
  736. }
  737. for _, ingredient := range ingredients {
  738. name := strings.ToLower(ingredient.Name)
  739. categorized := false
  740. // Check spices
  741. for _, keyword := range spiceKeywords {
  742. if strings.Contains(name, keyword) {
  743. shoppingPlan.Spices = append(shoppingPlan.Spices, ingredient.Name)
  744. categorized = true
  745. break
  746. }
  747. }
  748. if !categorized {
  749. // Check fresh items
  750. for _, keyword := range freshKeywords {
  751. if strings.Contains(name, keyword) {
  752. shoppingPlan.Fresh = append(shoppingPlan.Fresh, ingredient.Name)
  753. categorized = true
  754. break
  755. }
  756. }
  757. }
  758. if !categorized {
  759. // Check pantry items
  760. for _, keyword := range pantryKeywords {
  761. if strings.Contains(name, keyword) {
  762. shoppingPlan.Pantry = append(shoppingPlan.Pantry, ingredient.Name)
  763. categorized = true
  764. break
  765. }
  766. }
  767. }
  768. if !categorized {
  769. // Default to others
  770. shoppingPlan.Others = append(shoppingPlan.Others, ingredient.Name)
  771. }
  772. }
  773. }