h2oaimichalmarszalek commited on
Commit
4729622
·
1 Parent(s): d3b823a

final solution

Browse files
Files changed (4) hide show
  1. agent.py +11 -18
  2. app.py +16 -6
  3. local_development.py +23 -24
  4. tools/utils.py +276 -19
agent.py CHANGED
@@ -3,32 +3,26 @@ import os
3
  from pathlib import Path
4
  from typing import Optional
5
  from smolagents import CodeAgent, PythonInterpreterTool, WikipediaSearchTool, VisitWebpageTool, FinalAnswerTool
6
- from tools.utils import reverse_string, process_excel_file, is_text_file, execute_python_file, get_ingredients
7
  from tools.youtube import load_youtube
8
  from tools.audio import transcribe_audio
9
  from tools.web import optimized_web_search
10
 
11
- # from langchain.agents import load_tools
12
-
13
- # model = LiteLLMModel(
14
- # model_id="ollama/qwen2.5:7b",
15
- # api_base="http://localhost:11434"
16
- # )
17
 
18
  class BasicAgent:
19
- def __init__(self, model):
20
  self._model = model
21
  self._agent = CodeAgent(
22
- tools=[PythonInterpreterTool(), WikipediaSearchTool(), VisitWebpageTool(), FinalAnswerTool(),optimized_web_search, reverse_string, process_excel_file, is_text_file, load_youtube, execute_python_file, transcribe_audio, get_ingredients],
23
  additional_authorized_imports=['*', 'subprocess','markdownify', 'chess', 'random', 'time', 'itertools', 'pandas', 'webbrowser', 'requests', 'beautifulsoup4', 'csv', 'openpyxl', 'json', 'yaml'],
24
  model=model,
25
  add_base_tools=True,
26
- max_steps=10
27
  )
28
  print("BasicAgent initialized.")
29
 
30
  def __call__(self, question: str, file_path: Optional[str] = None) -> str:
31
- # print(f"Agent received question (first 50 chars): {question[:50]}...")
32
  if file_path and os.path.exists(file_path):
33
  try:
34
  file = Path(file_path)
@@ -36,17 +30,16 @@ class BasicAgent:
36
  with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
37
  file_content = f.read()
38
  if file.suffix == '.py':
39
- question += f"\nAttached Python code:\n{file_path}"
40
  else:
41
- question += f"\nAttached File Content:\n{file_content}"
42
  else:
43
  if file.suffix == '.mp3':
44
- question += f"\nUse tool transcribe_audio to extract text from mp3 file.\nAttached mp3 file to transcribes:\n{file_path}"
45
  else:
46
- question += f"\nAttached File Path:\n{file_path}"
47
  except Exception as e:
48
  print(f"Failed to read file: {e}")
49
- print(question)
50
- answer = self._agent.run(question)
51
- print(f"Agent returning answer: {answer}")
52
  return answer
 
3
  from pathlib import Path
4
  from typing import Optional
5
  from smolagents import CodeAgent, PythonInterpreterTool, WikipediaSearchTool, VisitWebpageTool, FinalAnswerTool
6
+ from tools.utils import reverse_string, process_excel_file, is_text_file, execute_python_file
7
  from tools.youtube import load_youtube
8
  from tools.audio import transcribe_audio
9
  from tools.web import optimized_web_search
10
 
 
 
 
 
 
 
11
 
12
  class BasicAgent:
13
+ def __init__(self, model, max_steps: int = 10):
14
  self._model = model
15
  self._agent = CodeAgent(
16
+ tools=[PythonInterpreterTool(), WikipediaSearchTool(), VisitWebpageTool(), FinalAnswerTool(), optimized_web_search, reverse_string, process_excel_file, is_text_file, load_youtube, execute_python_file, transcribe_audio],
17
  additional_authorized_imports=['*', 'subprocess','markdownify', 'chess', 'random', 'time', 'itertools', 'pandas', 'webbrowser', 'requests', 'beautifulsoup4', 'csv', 'openpyxl', 'json', 'yaml'],
18
  model=model,
19
  add_base_tools=True,
20
+ max_steps=max_steps
21
  )
22
  print("BasicAgent initialized.")
23
 
24
  def __call__(self, question: str, file_path: Optional[str] = None) -> str:
25
+ prompt = question
26
  if file_path and os.path.exists(file_path):
27
  try:
28
  file = Path(file_path)
 
30
  with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
31
  file_content = f.read()
32
  if file.suffix == '.py':
33
+ prompt += f"\nAttached Python code:\n{file_path}"
34
  else:
35
+ prompt += f"\nAttached File Content:\n{file_content}"
36
  else:
37
  if file.suffix == '.mp3':
38
+ prompt += f"\nUse tool transcribe_audio to extract text from mp3 file.\nAttached mp3 file to transcribes:\n{file_path}"
39
  else:
40
+ prompt += f"\nAttached File Path:\n{file_path}"
41
  except Exception as e:
42
  print(f"Failed to read file: {e}")
43
+ print(prompt)
44
+ answer = self._agent.run(prompt)
 
45
  return answer
app.py CHANGED
@@ -5,9 +5,10 @@ import requests
5
  import inspect
6
  import pandas as pd
7
  import backoff
 
8
 
9
  from agent import BasicAgent
10
- from smolagents import LiteLLMModel
11
  from tools.utils import download_file
12
 
13
  # (Keep Constants as is)
@@ -18,7 +19,7 @@ DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
18
  @backoff.on_exception(backoff.expo, Exception, max_tries=8, max_time=60)
19
  def submit(submit_url: str, submission_data):
20
  try:
21
- with open('answears.log', 'w') as s:
22
  s.write(json.dumps(submission_data))
23
  except:
24
  pass
@@ -48,11 +49,19 @@ def run_and_submit_all( profile: gr.OAuthProfile | None):
48
 
49
  # 1. Instantiate Agent ( modify this part to create your agent)
50
  try:
51
- model = LiteLLMModel(
52
- model_id="ollama/qwen2.5:7b",
53
- api_base="http://localhost:11434"
 
 
 
 
 
 
 
54
  )
55
- agent = BasicAgent(model)
 
56
  except Exception as e:
57
  print(f"Error instantiating agent: {e}")
58
  return f"Error initializing agent: {e}", None
@@ -86,6 +95,7 @@ def run_and_submit_all( profile: gr.OAuthProfile | None):
86
  answers_payload = []
87
  file_path = None
88
  print(f"Running agent on {len(questions_data)} questions...")
 
89
  for item in questions_data:
90
  task_id = item.get("task_id")
91
  file_name = item.get('file_name')
 
5
  import inspect
6
  import pandas as pd
7
  import backoff
8
+ import time
9
 
10
  from agent import BasicAgent
11
+ from smolagents import OpenAIServerModel
12
  from tools.utils import download_file
13
 
14
  # (Keep Constants as is)
 
19
  @backoff.on_exception(backoff.expo, Exception, max_tries=8, max_time=60)
20
  def submit(submit_url: str, submission_data):
21
  try:
22
+ with open('answears.json', 'w') as s:
23
  s.write(json.dumps(submission_data))
24
  except:
25
  pass
 
49
 
50
  # 1. Instantiate Agent ( modify this part to create your agent)
51
  try:
52
+ # model = LiteLLMModel(
53
+ # model_id="ollama/qwen2.5:7b",
54
+ # api_base="http://localhost:11434"
55
+ # )
56
+
57
+ # Final solution uses mistral-medium-2505
58
+ model = OpenAIServerModel(
59
+ model_id="mistral-medium-2505", #the model name in the Mistral documentation
60
+ api_base="https://api.mistral.ai/v1", # URL endpoints Mistral
61
+ api_key=os.getenv('MISTRAL_API_KEY'),
62
  )
63
+
64
+ agent = BasicAgent(model, max_steps=10)
65
  except Exception as e:
66
  print(f"Error instantiating agent: {e}")
67
  return f"Error initializing agent: {e}", None
 
95
  answers_payload = []
96
  file_path = None
97
  print(f"Running agent on {len(questions_data)} questions...")
98
+
99
  for item in questions_data:
100
  task_id = item.get("task_id")
101
  file_name = item.get('file_name')
local_development.py CHANGED
@@ -1,6 +1,5 @@
1
  import argparse
2
  import json
3
- import random
4
  from agent import BasicAgent
5
  from smolagents import LiteLLMModel
6
  from tools.utils import download_file
@@ -8,7 +7,9 @@ from tools.utils import download_file
8
 
9
  if __name__ == '__main__':
10
  parser = argparse.ArgumentParser()
11
- parser.add_argument("--id", required=False, type=str, help="Number of question to load", default=None)
 
 
12
  args = parser.parse_args()
13
  questions = []
14
  question = None
@@ -17,28 +18,26 @@ if __name__ == '__main__':
17
 
18
  with open('questions.json', 'r') as s:
19
  questions = json.load(s)
20
- if args.id:
21
- question = [q for q in questions if q['task_id'] == args.id][0]
22
- print(f"Process question: {question.get('question')[:50]}")
23
- else:
24
- n = random.randint(0, len(questions))
25
- question = questions[n]
26
- print(f"Process random question: {question.get('question')[:50]}")
27
 
28
- file_name = question.get('file_name')
29
- prompt = question.get('question')
30
-
31
- if file_name:
32
  task_id = question.get('task_id')
33
- file_path = download_file(f'{base_url}/files/{task_id}', file_name)
34
- else:
35
- file_path = None
36
-
 
37
 
38
- model = LiteLLMModel(
39
- model_id="ollama/qwen2.5:7b",
40
- api_base="http://localhost:11434"
41
- )
42
- agent = BasicAgent(model)
43
- response = agent(prompt, file_path)
44
- print(response)
 
 
 
1
  import argparse
2
  import json
 
3
  from agent import BasicAgent
4
  from smolagents import LiteLLMModel
5
  from tools.utils import download_file
 
7
 
8
  if __name__ == '__main__':
9
  parser = argparse.ArgumentParser()
10
+ parser.add_argument('--id', required=False, action="append", help="Number of question to load")
11
+ parser.add_argument('--max', required=False, type=int, default=10 , help="Number of max steps")
12
+ answears = {}
13
  args = parser.parse_args()
14
  questions = []
15
  question = None
 
18
 
19
  with open('questions.json', 'r') as s:
20
  questions = json.load(s)
21
+ if args.id:
22
+ questions = [q for q in questions if q.get('task_id') in args.id]
 
 
 
 
 
23
 
24
+ for question in questions:
25
+ print(f"Process question: {question.get('question')[:50]}")
26
+ file_name = question.get('file_name')
27
+ prompt = question.get('question')
28
  task_id = question.get('task_id')
29
+
30
+ if file_name:
31
+ file_path = download_file(f'{base_url}/files/{task_id}', file_name)
32
+ else:
33
+ file_path = None
34
 
35
+ model = LiteLLMModel(
36
+ model_id="ollama/qwen2.5:7b",
37
+ api_base="http://localhost:11434"
38
+ )
39
+ agent = BasicAgent(model, args.max)
40
+ response = agent(prompt, file_path)
41
+ answears[task_id] = response
42
+
43
+ print(answears)
tools/utils.py CHANGED
@@ -179,6 +179,280 @@ def execute_python_file(file_path: str) -> str:
179
  except Exception as e:
180
  return f"Error: Unexpected error: {str(e)}"
181
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  @tool
183
  def get_ingredients(item: str) -> str:
184
  """
@@ -208,27 +482,10 @@ def get_ingredients(item: str) -> str:
208
  >>> get_ingredients("APPLE Carrot SUGAR")
209
  "apple,carrot,sugar"
210
  """
211
- # Lists of common fruits, vegetables, and products
212
- fruits = {
213
- 'apple', 'banana', 'orange', 'strawberry', 'blueberry', 'raspberry',
214
- 'mango', 'pineapple', 'grape', 'kiwi', 'peach', 'pear', 'plum',
215
- 'cherry', 'lemon', 'lime', 'avocado', 'pomegranate', 'fig', 'date'
216
- }
217
-
218
- vegetables = {
219
- 'carrot', 'broccoli', 'spinach', 'tomato', 'cucumber', 'lettuce',
220
- 'onion', 'garlic', 'potato', 'sweet potato', 'zucchini', 'pepper',
221
- 'eggplant', 'cauliflower', 'cabbage', 'kale', 'mushroom', 'celery'
222
- }
223
-
224
- products = {
225
- 'vanilla', 'sugar', 'flour', 'salt', 'pepper', 'oil', 'butter',
226
- 'milk', 'cream', 'cheese', 'yogurt', 'egg', 'honey', 'vinegar',
227
- 'baking powder', 'baking soda', 'yeast', 'cinnamon', 'cocoa'
228
- }
229
 
230
  def is_ingredient(ingredient: str) -> bool:
231
- return ingredient in fruits or ingredient in vegetables or ingredient in products
232
 
233
  items = set([x.lower().strip() for x in item.split() if is_ingredient(x)])
234
  return ','.join(sorted(items))
 
179
  except Exception as e:
180
  return f"Error: Unexpected error: {str(e)}"
181
 
182
+
183
+ @tool
184
+ def plural_to_singular(word: str) -> str:
185
+ """
186
+ Convert a plural word to its singular form.
187
+
188
+ This function handles common English pluralization patterns including:
189
+ - Regular plurals ending in 's' (cats -> cat)
190
+ - Words ending in 'ies' (flies -> fly)
191
+ - Words ending in 'ves' (knives -> knife)
192
+ - Words ending in 'es' after sibilants (boxes -> box)
193
+ - Irregular plurals (children -> child, feet -> foot, etc.)
194
+
195
+ Args:
196
+ word (str): The plural word to convert to singular form.
197
+
198
+ Returns:
199
+ str: The singular form of the word.
200
+
201
+ Examples:
202
+ >>> plural_to_singular("cats")
203
+ 'cat'
204
+ >>> plural_to_singular("flies")
205
+ 'fly'
206
+ >>> plural_to_singular("children")
207
+ 'child'
208
+ >>> plural_to_singular("boxes")
209
+ 'box'
210
+ """
211
+ if not word or not isinstance(word, str):
212
+ return word
213
+
214
+ word = word.lower().strip()
215
+
216
+ # Handle irregular plurals
217
+ irregular_plurals = {
218
+ 'children': 'child',
219
+ 'feet': 'foot',
220
+ 'teeth': 'tooth',
221
+ 'geese': 'goose',
222
+ 'mice': 'mouse',
223
+ 'men': 'man',
224
+ 'women': 'woman',
225
+ 'people': 'person',
226
+ 'oxen': 'ox',
227
+ 'sheep': 'sheep',
228
+ 'deer': 'deer',
229
+ 'fish': 'fish',
230
+ 'species': 'species',
231
+ 'series': 'series'
232
+ }
233
+
234
+ if word in irregular_plurals:
235
+ return irregular_plurals[word]
236
+
237
+ # Handle words ending in 'ies' -> 'y'
238
+ if word.endswith('ies') and len(word) > 3:
239
+ return word[:-3] + 'y'
240
+
241
+ # Handle words ending in 'ves' -> 'f' or 'fe'
242
+ if word.endswith('ves'):
243
+ if word[:-3] + 'f' in ['leaf', 'loaf', 'thief', 'shelf', 'knife', 'life', 'wife']:
244
+ return word[:-3] + 'f'
245
+ elif word == 'wolves':
246
+ return 'wolf'
247
+ elif word == 'calves':
248
+ return 'calf'
249
+ elif word == 'halves':
250
+ return 'half'
251
+
252
+ # Handle words ending in 'es' after sibilants (s, ss, sh, ch, x, z)
253
+ if word.endswith('es') and len(word) > 2:
254
+ if word[-3:-2] in ['s', 'x', 'z'] or word[-4:-2] in ['sh', 'ch', 'ss']:
255
+ return word[:-2]
256
+
257
+ # Handle words ending in 'oes' -> 'o'
258
+ if word.endswith('oes') and len(word) > 3:
259
+ return word[:-2]
260
+
261
+ # Handle regular plurals ending in 's'
262
+ if word.endswith('s') and len(word) > 1:
263
+ return word[:-1]
264
+
265
+ # If no pattern matches, return the original word
266
+ return word
267
+
268
+ @tool
269
+ def is_fruit(item: str) -> bool:
270
+ """
271
+ Check if the given item is a recognized fruit.
272
+
273
+ This function determines whether the provided string matches one of the
274
+ predefined fruits in the internal fruit list. The input is automatically
275
+ converted to singular form and stripped of whitespace before comparison.
276
+
277
+ The recognized fruits include common varieties such as:
278
+ - Tree fruits: apple, orange, peach, pear, plum, cherry, lemon, lime
279
+ - Berries: strawberry, blueberry, raspberry, grape
280
+ - Tropical fruits: mango, pineapple, kiwi, avocado, pomegranate
281
+ - Dried fruits: fig, date
282
+ - And more: banana
283
+
284
+ Args:
285
+ item (str): The item name to check against the fruit list. Can be
286
+ singular or plural form.
287
+
288
+ Returns:
289
+ bool: True if the item is a recognized fruit, False otherwise.
290
+
291
+ Examples:
292
+ >>> is_fruit("apple")
293
+ True
294
+ >>> is_fruit("apples")
295
+ True
296
+ >>> is_fruit(" Strawberries ")
297
+ True
298
+ >>> is_fruit("carrot")
299
+ False
300
+ >>> is_fruit("banana")
301
+ True
302
+
303
+ Note:
304
+ The function uses plural_to_singular() to handle both singular and
305
+ plural forms of fruit names automatically.
306
+ """
307
+ item = plural_to_singular(item.strip())
308
+ fruits = {
309
+ 'apple', 'banana', 'orange', 'strawberry', 'blueberry', 'raspberry',
310
+ 'mango', 'pineapple', 'grape', 'kiwi', 'peach', 'pear', 'plum',
311
+ 'cherry', 'lemon', 'lime', 'avocado', 'pomegranate', 'fig', 'date'
312
+ }
313
+ return item in fruits
314
+
315
+ @tool
316
+ def is_vegetable(item: str) -> bool:
317
+ """
318
+ Check if the given item is a recognized vegetable.
319
+
320
+ This function determines whether the provided string matches one of the
321
+ predefined vegetables in the internal vegetable list. The input is
322
+ automatically converted to singular form and stripped of whitespace
323
+ before comparison.
324
+
325
+ The recognized vegetables include various categories:
326
+ - Root vegetables: carrot, onion, garlic, potato, sweet potato
327
+ - Leafy greens: spinach, lettuce, kale, cabbage
328
+ - Cruciferous: broccoli, cauliflower, cabbage, kale
329
+ - Nightshades: tomato, pepper, eggplant
330
+ - Squash family: zucchini
331
+ - Other: cucumber, celery, mushroom
332
+
333
+ Args:
334
+ item (str): The item name to check against the vegetable list. Can be
335
+ singular or plural form.
336
+
337
+ Returns:
338
+ bool: True if the item is a recognized vegetable, False otherwise.
339
+
340
+ Examples:
341
+ >>> is_vegetable("carrot")
342
+ True
343
+ >>> is_vegetable("carrots")
344
+ True
345
+ >>> is_vegetable(" BROCCOLI ")
346
+ True
347
+ >>> is_vegetable("apple")
348
+ False
349
+ >>> is_vegetable("mushrooms")
350
+ True
351
+
352
+ Note:
353
+ The function uses plural_to_singular() to handle both singular and
354
+ plural forms of vegetable names automatically. Note that some items
355
+ like tomatoes are botanically fruits but classified as vegetables
356
+ in culinary contexts.
357
+ """
358
+ item = plural_to_singular(item.strip())
359
+ vegetables = {
360
+ 'carrot', 'broccoli', 'spinach', 'tomato', 'cucumber', 'lettuce',
361
+ 'onion', 'garlic', 'potato', 'sweet potato', 'zucchini', 'pepper',
362
+ 'eggplant', 'cauliflower', 'cabbage', 'kale', 'mushroom', 'celery'
363
+ }
364
+ return item in vegetables
365
+
366
+ @tool
367
+ def is_product(item: str) -> bool:
368
+ """
369
+ Check if the given item is a recognized food product or ingredient.
370
+
371
+ This function determines whether the provided string matches one of the
372
+ predefined food products in the internal product list. The input is
373
+ automatically converted to singular form and stripped of whitespace
374
+ before comparison.
375
+
376
+ The recognized products include various categories:
377
+ - Baking ingredients: flour, sugar, salt, baking powder, baking soda, yeast
378
+ - Spices and flavorings: pepper, cinnamon, vanilla, honey
379
+ - Dairy products: milk, cream, cheese, yogurt, butter
380
+ - Cooking essentials: oil, vinegar, egg
381
+ - Beverages and treats: juice, ice cream
382
+ - Specialty items: cocoa
383
+
384
+ Args:
385
+ item (str): The item name to check against the product list. Can be
386
+ singular or plural form.
387
+
388
+ Returns:
389
+ bool: True if the item is a recognized food product, False otherwise.
390
+
391
+ Examples:
392
+ >>> is_product("flour")
393
+ True
394
+ >>> is_product("eggs")
395
+ True
396
+ >>> is_product(" Baking Powder ")
397
+ True
398
+ >>> is_product("carrot")
399
+ False
400
+ >>> is_product("ice cream")
401
+ True
402
+
403
+ Note:
404
+ The function uses plural_to_singular() to handle both singular and
405
+ plural forms of product names automatically. Some items like
406
+ "ice cream" are treated as compound terms and matched exactly.
407
+ """
408
+ item = plural_to_singular(item.strip())
409
+ products = {
410
+ 'vanilla', 'sugar', 'flour', 'salt', 'pepper', 'oil', 'butter',
411
+ 'milk', 'cream', 'cheese', 'yogurt', 'egg', 'honey', 'vinegar',
412
+ 'baking powder', 'baking soda', 'yeast', 'cinnamon', 'cocoa', 'juice', 'ice cream'
413
+ }
414
+ return item in products
415
+
416
+ @tool
417
+ def is_food(item: str) -> bool:
418
+ """
419
+ Check if the given item is a recognized food item.
420
+
421
+ This function determines whether the provided string matches one of the
422
+ predefined food items in the internal food list. The comparison is
423
+ case-insensitive and ignores leading/trailing whitespace.
424
+
425
+ The recognized food items are:
426
+ - burgers
427
+ - hot dogs
428
+ - salads
429
+ - fries
430
+ - ice cream
431
+
432
+ Args:
433
+ item (str): The item name to check against the food list.
434
+
435
+ Returns:
436
+ bool: True if the item is a recognized food item, False otherwise.
437
+
438
+ Examples:
439
+ >>> is_food("burgers")
440
+ True
441
+ >>> is_food("FRIES")
442
+ True
443
+ >>> is_food(" Ice Cream ")
444
+ True
445
+ >>> is_food("pizza")
446
+ False
447
+ >>> is_food("books")
448
+ False
449
+
450
+ Note:
451
+ The function performs case-insensitive matching and automatically
452
+ strips leading and trailing whitespace from the input.
453
+ """
454
+ return item.lower().strip() in ('burgers', 'hot dogs', 'salads', 'fries', 'ice cream')
455
+
456
  @tool
457
  def get_ingredients(item: str) -> str:
458
  """
 
482
  >>> get_ingredients("APPLE Carrot SUGAR")
483
  "apple,carrot,sugar"
484
  """
485
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
486
 
487
  def is_ingredient(ingredient: str) -> bool:
488
+ return is_fruit(ingredient) or is_vegetable(ingredient) or is_product(ingredient)
489
 
490
  items = set([x.lower().strip() for x in item.split() if is_ingredient(x)])
491
  return ','.join(sorted(items))