fix(mcp): status active + search_products guard + item.product + cleanup auto-name
This commit is contained in:
@@ -361,19 +361,19 @@ async def get_shopping_lists() -> str:
|
|||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def get_active_shopping_list() -> str:
|
async def get_active_shopping_list() -> str:
|
||||||
"""Retourne la première liste en statut 'draft' avec tous ses articles triés."""
|
"""Retourne la première liste en statut 'draft' ou 'active' avec tous ses articles triés."""
|
||||||
async with AsyncSessionLocal() as session:
|
async with AsyncSessionLocal() as session:
|
||||||
stmt = (
|
stmt = (
|
||||||
select(ShoppingList)
|
select(ShoppingList)
|
||||||
.where(ShoppingList.status == "draft")
|
.where(ShoppingList.status.in_(["draft", "active"]))
|
||||||
.options(selectinload(ShoppingList.items).selectinload(ListItem.product))
|
.options(selectinload(ShoppingList.items).selectinload(ListItem.product))
|
||||||
.order_by(ShoppingList.created_at.desc())
|
.order_by(ShoppingList.status.asc(), ShoppingList.created_at.desc())
|
||||||
.limit(1)
|
.limit(1)
|
||||||
)
|
)
|
||||||
result = await session.execute(stmt)
|
result = await session.execute(stmt)
|
||||||
lst = result.scalar_one_or_none()
|
lst = result.scalar_one_or_none()
|
||||||
if not lst:
|
if not lst:
|
||||||
return _dumps({"error": "Aucune liste active (statut draft)"})
|
return _dumps({"error": "Aucune liste active (statut draft ou active)"})
|
||||||
sorted_items = sorted(lst.items, key=lambda i: (i.sort_order or 999, str(i.id)))
|
sorted_items = sorted(lst.items, key=lambda i: (i.sort_order or 999, str(i.id)))
|
||||||
return _dumps({
|
return _dumps({
|
||||||
"id": lst.id,
|
"id": lst.id,
|
||||||
@@ -394,6 +394,8 @@ async def get_active_shopping_list() -> str:
|
|||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def search_products(q: str) -> str:
|
async def search_products(q: str) -> str:
|
||||||
"""Recherche dans le catalogue produits par nom, description ou catégorie."""
|
"""Recherche dans le catalogue produits par nom, description ou catégorie."""
|
||||||
|
if not q or not q.strip():
|
||||||
|
return _dumps({"error": "Paramètre q requis"})
|
||||||
async with AsyncSessionLocal() as session:
|
async with AsyncSessionLocal() as session:
|
||||||
stmt = (
|
stmt = (
|
||||||
select(Product)
|
select(Product)
|
||||||
@@ -490,19 +492,18 @@ async def check_shopping_item(list_id: str, item_id: str) -> str:
|
|||||||
return _dumps({"error": f"Article introuvable : {item_id} dans liste {list_id}"})
|
return _dumps({"error": f"Article introuvable : {item_id} dans liste {list_id}"})
|
||||||
was_checked = item.is_checked
|
was_checked = item.is_checked
|
||||||
item.is_checked = True
|
item.is_checked = True
|
||||||
if not was_checked and item.product_id:
|
if not was_checked and item.product:
|
||||||
product = await session.get(Product, item.product_id)
|
product = item.product
|
||||||
if product:
|
today = date_type.today()
|
||||||
today = date_type.today()
|
if product.last_purchased_at and product.last_purchased_at < today:
|
||||||
if product.last_purchased_at and product.last_purchased_at < today:
|
days = (today - product.last_purchased_at).days
|
||||||
days = (today - product.last_purchased_at).days
|
if product.avg_interval_days is None:
|
||||||
if product.avg_interval_days is None:
|
product.avg_interval_days = Decimal(str(days))
|
||||||
product.avg_interval_days = Decimal(str(days))
|
else:
|
||||||
else:
|
product.avg_interval_days = Decimal(str(
|
||||||
product.avg_interval_days = Decimal(str(
|
round(float(product.avg_interval_days) * 0.7 + days * 0.3, 1)
|
||||||
round(float(product.avg_interval_days) * 0.7 + days * 0.3, 1)
|
))
|
||||||
))
|
product.last_purchased_at = today
|
||||||
product.last_purchased_at = today
|
product.frequency_score += 1
|
||||||
product.frequency_score += 1
|
|
||||||
await session.commit()
|
await session.commit()
|
||||||
return _dumps({"id": item.id, "is_checked": item.is_checked})
|
return _dumps({"id": item.id, "is_checked": item.is_checked})
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import json
|
import json
|
||||||
|
import uuid
|
||||||
import pytest
|
import pytest
|
||||||
from sqlalchemy import delete
|
from sqlalchemy import delete
|
||||||
import app.api.mcp_server as mcp_server_module
|
import app.api.mcp_server as mcp_server_module
|
||||||
@@ -155,7 +156,14 @@ async def test_create_shopping_list_outil():
|
|||||||
async def test_create_shopping_list_nom_auto():
|
async def test_create_shopping_list_nom_auto():
|
||||||
result = await create_shopping_list()
|
result = await create_shopping_list()
|
||||||
data = json.loads(result)
|
data = json.loads(result)
|
||||||
|
# Le nom auto est au format "S{semaine} {année}" (ex: "S22 2026")
|
||||||
assert data["name"].startswith("S")
|
assert data["name"].startswith("S")
|
||||||
|
# Cleanup manuel — la liste auto n'a pas le préfixe TEST_MCP_
|
||||||
|
async with mcp_server_module.AsyncSessionLocal() as session:
|
||||||
|
await session.execute(
|
||||||
|
delete(ShoppingList).where(ShoppingList.id == uuid.UUID(data["id"]))
|
||||||
|
)
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
|
||||||
async def test_add_shopping_item_outil():
|
async def test_add_shopping_item_outil():
|
||||||
|
|||||||
Reference in New Issue
Block a user