fixed rp api
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.routes import character, chat, fitness, health, homelab, media, memory, pomodoro, projects, webhooks
|
||||
from app.api.routes import character, chat, fitness, health, homelab, media, memory, pomodoro, projects, shopping, webhooks
|
||||
|
||||
api_router = APIRouter(prefix="/api/v1")
|
||||
api_router.include_router(health.router, tags=["health"])
|
||||
@@ -11,5 +11,6 @@ api_router.include_router(character.router, tags=["character"])
|
||||
api_router.include_router(projects.router, tags=["projects"])
|
||||
api_router.include_router(memory.router, tags=["memory"])
|
||||
api_router.include_router(fitness.router, tags=["fitness"])
|
||||
api_router.include_router(shopping.router, prefix="/shopping", tags=["shopping"])
|
||||
api_router.include_router(webhooks.router, tags=["webhooks"])
|
||||
api_router.include_router(media.router, tags=["media"])
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.base import get_db
|
||||
from app.shopping.service import ShoppingService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class ListCreate(BaseModel):
|
||||
name: str = Field(min_length=1, max_length=255)
|
||||
|
||||
|
||||
class ListRename(BaseModel):
|
||||
name: str = Field(min_length=1, max_length=255)
|
||||
|
||||
|
||||
class ItemInput(BaseModel):
|
||||
text: str = Field(min_length=1, max_length=500)
|
||||
quantity: float | None = None
|
||||
unit: str = ""
|
||||
|
||||
|
||||
class ItemsAdd(BaseModel):
|
||||
list_id: int | None = None
|
||||
list_name: str | None = None
|
||||
items: list[ItemInput] = Field(min_length=1)
|
||||
|
||||
|
||||
class ItemChecked(BaseModel):
|
||||
checked: bool
|
||||
|
||||
|
||||
@router.get("")
|
||||
def get_snapshot(db: Session = Depends(get_db)) -> dict[str, Any]:
|
||||
return ShoppingService(db).snapshot()
|
||||
|
||||
|
||||
@router.get("/lists")
|
||||
def list_lists(db: Session = Depends(get_db)) -> list[dict[str, Any]]:
|
||||
return ShoppingService(db).list_lists(include_items=True)
|
||||
|
||||
|
||||
@router.post("/lists")
|
||||
def create_list(payload: ListCreate, db: Session = Depends(get_db)) -> dict[str, Any]:
|
||||
try:
|
||||
return ShoppingService(db).create_list(payload.name)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.get("/lists/{list_id}")
|
||||
def get_list(list_id: int, db: Session = Depends(get_db)) -> dict[str, Any]:
|
||||
data = ShoppingService(db).get_list(list_id=list_id)
|
||||
if not data:
|
||||
raise HTTPException(status_code=404, detail="List not found")
|
||||
return data
|
||||
|
||||
|
||||
@router.patch("/lists/{list_id}")
|
||||
def rename_list(list_id: int, payload: ListRename, db: Session = Depends(get_db)) -> dict[str, Any]:
|
||||
try:
|
||||
return ShoppingService(db).rename_list(list_id, payload.name)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.delete("/lists/{list_id}")
|
||||
def delete_list(list_id: int, db: Session = Depends(get_db)) -> dict[str, Any]:
|
||||
try:
|
||||
return ShoppingService(db).delete_list(list_id)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/items")
|
||||
def add_items(payload: ItemsAdd, db: Session = Depends(get_db)) -> dict[str, Any]:
|
||||
try:
|
||||
return ShoppingService(db).add_items(
|
||||
[i.model_dump() for i in payload.items],
|
||||
list_id=payload.list_id,
|
||||
list_name=payload.list_name,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.patch("/items/{item_id}")
|
||||
def set_item_checked(
|
||||
item_id: int,
|
||||
payload: ItemChecked,
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
return ShoppingService(db).set_item_checked(item_id, payload.checked)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.delete("/items/{item_id}")
|
||||
def remove_item(item_id: int, db: Session = Depends(get_db)) -> dict[str, Any]:
|
||||
try:
|
||||
return ShoppingService(db).remove_item(item_id)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/lists/{list_id}/clear-checked")
|
||||
def clear_checked(list_id: int, db: Session = Depends(get_db)) -> dict[str, Any]:
|
||||
try:
|
||||
return ShoppingService(db).clear_checked(list_id)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
@@ -23,6 +23,8 @@ TOOLS_INSTRUCTIONS = """
|
||||
- Погода: get_weather или блок [Погода] в контексте; «что на улице» / «будет ли дождь» — не выдумывай.
|
||||
- Утренний брифинг (погода + новости) → get_morning_briefing.
|
||||
- Картинки: generate_image — «нарисуй себя» → draw_self=true; иначе scene_description на английском (booru-теги). Внешность из карточки персонажа. Не злоупотребляй.
|
||||
- Покупки: list_shopping_lists, create_shopping_list, add_shopping_items, check_shopping_item, remove_shopping_item, delete_shopping_list.
|
||||
- «Добавь в список покупок» → add_shopping_items (list_name + товары). «Что купить» → list_shopping_lists. Не выдумывай списки.
|
||||
""".strip()
|
||||
|
||||
DEFAULT_CARD: dict[str, Any] = {
|
||||
|
||||
@@ -69,6 +69,15 @@ FITNESS_TOOL_NAMES = frozenset({
|
||||
})
|
||||
|
||||
# Не засорять чат служебными ответами
|
||||
SHOPPING_TOOL_NAMES = frozenset({
|
||||
"list_shopping_lists",
|
||||
"create_shopping_list",
|
||||
"add_shopping_items",
|
||||
"check_shopping_item",
|
||||
"remove_shopping_item",
|
||||
"delete_shopping_list",
|
||||
})
|
||||
|
||||
TOOLS_SKIP_CHAT_NOTICE = frozenset({
|
||||
"get_pomodoro_status",
|
||||
"recall_memories",
|
||||
@@ -78,6 +87,7 @@ TOOLS_SKIP_CHAT_NOTICE = frozenset({
|
||||
"calc_fitness_targets",
|
||||
"get_weather",
|
||||
"get_morning_briefing",
|
||||
"list_shopping_lists",
|
||||
})
|
||||
|
||||
|
||||
@@ -97,6 +107,8 @@ def format_tool_notice(tool_name: str, raw_result: str) -> str | None:
|
||||
prefix = "🧠"
|
||||
elif tool_name in FITNESS_TOOL_NAMES:
|
||||
prefix = "💪"
|
||||
elif tool_name in SHOPPING_TOOL_NAMES:
|
||||
prefix = "🛒"
|
||||
else:
|
||||
prefix = "📋"
|
||||
return f"{prefix} {data['error']}"
|
||||
@@ -196,6 +208,29 @@ def format_tool_notice(tool_name: str, raw_result: str) -> str | None:
|
||||
url = data.get("url", "")
|
||||
return f"🎨 **Картинка готова**\n\n"
|
||||
|
||||
if tool_name == "create_shopping_list" and data.get("ok"):
|
||||
lst = data.get("list") or {}
|
||||
action = "создан" if data.get("created") else "уже был"
|
||||
return f"🛒 **Список {action}** · «{lst.get('name')}» (#{lst.get('id')})"
|
||||
|
||||
if tool_name == "add_shopping_items" and data.get("ok"):
|
||||
added = data.get("added") or []
|
||||
names = ", ".join(i.get("text", "") for i in added[:5])
|
||||
extra = f" +{len(added) - 5}" if len(added) > 5 else ""
|
||||
return f"🛒 **Добавлено в «{data.get('list_name')}»** · {names}{extra}"
|
||||
|
||||
if tool_name == "check_shopping_item" and data.get("ok"):
|
||||
item = data.get("item") or {}
|
||||
state = "куплено" if item.get("checked") else "снята отметка"
|
||||
return f"🛒 **{state}** · #{item.get('id')} {item.get('text')}"
|
||||
|
||||
if tool_name == "remove_shopping_item" and data.get("ok"):
|
||||
removed = data.get("removed") or {}
|
||||
return f"🛒 **Удалено** · {removed.get('text')}"
|
||||
|
||||
if tool_name == "delete_shopping_list" and data.get("ok"):
|
||||
return f"🛒 **Список удалён** · «{data.get('name')}»"
|
||||
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ from app.memory.context import (
|
||||
)
|
||||
from app.memory.extract import extract_after_turn
|
||||
from app.projects.context import format_projects_context, get_projects_snapshot
|
||||
from app.shopping.context import format_shopping_context, get_shopping_snapshot
|
||||
from app.db.models import ChatSession, Message
|
||||
from app.llm.client import LLMClient
|
||||
from app.pomodoro.service import PomodoroService
|
||||
@@ -63,12 +64,14 @@ class ChatService:
|
||||
status = PomodoroService(self.db).get_status()
|
||||
memory_snapshot = get_memory_snapshot(self.db, session_id)
|
||||
fitness_snapshot = get_fitness_snapshot(self.db)
|
||||
shopping_snapshot = get_shopping_snapshot(self.db)
|
||||
projects_snapshot = get_projects_snapshot(self.db)
|
||||
return (
|
||||
f"{self.character.get_system_prompt()}\n\n"
|
||||
f"{format_datetime_context(self.db)}\n\n"
|
||||
f"{format_memory_context(memory_snapshot)}\n\n"
|
||||
f"{format_fitness_context(fitness_snapshot)}\n\n"
|
||||
f"{format_shopping_context(shopping_snapshot)}\n\n"
|
||||
f"{format_weather_snapshot()}\n\n"
|
||||
f"{format_pomodoro_context(status)}\n\n"
|
||||
f"{format_projects_context(projects_snapshot)}"
|
||||
|
||||
@@ -215,6 +215,41 @@ class FitnessReminder(Base):
|
||||
last_fired_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
|
||||
class ShoppingList(Base):
|
||||
__tablename__ = "shopping_lists"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
name: Mapped[str] = mapped_column(String(255), unique=True, index=True)
|
||||
sort_order: Mapped[int] = mapped_column(Integer, default=0)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
items: Mapped[list["ShoppingListItem"]] = relationship(
|
||||
back_populates="shopping_list",
|
||||
cascade="all, delete-orphan",
|
||||
order_by="ShoppingListItem.sort_order, ShoppingListItem.id",
|
||||
)
|
||||
|
||||
|
||||
class ShoppingListItem(Base):
|
||||
__tablename__ = "shopping_list_items"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
list_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("shopping_lists.id", ondelete="CASCADE"), index=True
|
||||
)
|
||||
text: Mapped[str] = mapped_column(String(500))
|
||||
quantity: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
unit: Mapped[str] = mapped_column(String(64), default="")
|
||||
checked: Mapped[bool] = mapped_column(Boolean, default=False, index=True)
|
||||
sort_order: Mapped[int] = mapped_column(Integer, default=0)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
shopping_list: Mapped["ShoppingList"] = relationship(back_populates="items")
|
||||
|
||||
|
||||
class AssistantState(Base):
|
||||
__tablename__ = "assistant_state"
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.shopping.service import ShoppingService
|
||||
|
||||
MAX_LISTS_IN_CONTEXT = 8
|
||||
MAX_ITEMS_PER_LIST = 12
|
||||
|
||||
|
||||
def get_shopping_snapshot(db: Session) -> dict[str, Any]:
|
||||
return ShoppingService(db).snapshot()
|
||||
|
||||
|
||||
def format_shopping_context(snapshot: dict[str, Any]) -> str:
|
||||
lines = ["[Списки покупок]"]
|
||||
lists = snapshot.get("lists") or []
|
||||
|
||||
if not lists:
|
||||
lines.append("Списков пока нет. create_shopping_list или add_shopping_items.")
|
||||
return "\n".join(lines)
|
||||
|
||||
lines.append(
|
||||
f"Всего списков: {snapshot.get('list_count', len(lists))}, "
|
||||
f"неотмеченных позиций: {snapshot.get('unchecked_items', 0)}."
|
||||
)
|
||||
lines.append("Для изменений вызывай tools: list_shopping_lists, add_shopping_items, check_shopping_item.")
|
||||
|
||||
for lst in lists[:MAX_LISTS_IN_CONTEXT]:
|
||||
items = lst.get("items") or []
|
||||
unchecked = [i for i in items if not i.get("checked")]
|
||||
preview = unchecked[:MAX_ITEMS_PER_LIST]
|
||||
parts = []
|
||||
for item in preview:
|
||||
qty = item.get("quantity")
|
||||
unit = (item.get("unit") or "").strip()
|
||||
label = item["text"]
|
||||
if qty is not None:
|
||||
label = f"{label} ({qty}{' ' + unit if unit else ''})"
|
||||
parts.append(f"#{item['id']} {label}")
|
||||
tail = f" +{len(unchecked) - len(preview)} ещё" if len(unchecked) > len(preview) else ""
|
||||
if parts:
|
||||
lines.append(f"- «{lst['name']}» (#{lst['id']}): {', '.join(parts)}{tail}")
|
||||
else:
|
||||
lines.append(f"- «{lst['name']}» (#{lst['id']}): всё отмечено или пусто")
|
||||
|
||||
return "\n".join(lines)
|
||||
@@ -0,0 +1,223 @@
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import Session, selectinload
|
||||
|
||||
from app.db.models import ShoppingList, ShoppingListItem
|
||||
|
||||
|
||||
class ShoppingService:
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
|
||||
def snapshot(self) -> dict[str, Any]:
|
||||
lists = self.list_lists(include_items=True)
|
||||
total_items = sum(len(lst.get("items") or []) for lst in lists)
|
||||
unchecked = sum(
|
||||
1
|
||||
for lst in lists
|
||||
for item in (lst.get("items") or [])
|
||||
if not item.get("checked")
|
||||
)
|
||||
return {
|
||||
"lists": lists,
|
||||
"list_count": len(lists),
|
||||
"total_items": total_items,
|
||||
"unchecked_items": unchecked,
|
||||
}
|
||||
|
||||
def list_lists(self, *, include_items: bool = False) -> list[dict[str, Any]]:
|
||||
stmt = select(ShoppingList).order_by(ShoppingList.sort_order, ShoppingList.name)
|
||||
if include_items:
|
||||
stmt = stmt.options(selectinload(ShoppingList.items))
|
||||
rows = list(self.db.scalars(stmt).all())
|
||||
return [self._list_to_dict(row, include_items=include_items) for row in rows]
|
||||
|
||||
def get_list(
|
||||
self,
|
||||
list_id: int | None = None,
|
||||
*,
|
||||
name: str | None = None,
|
||||
) -> dict[str, Any] | None:
|
||||
row = self._resolve_list(list_id=list_id, name=name)
|
||||
if not row:
|
||||
return None
|
||||
return self._list_to_dict(row, include_items=True)
|
||||
|
||||
def create_list(self, name: str) -> dict[str, Any]:
|
||||
clean = name.strip()
|
||||
if not clean:
|
||||
raise ValueError("Название списка не может быть пустым")
|
||||
existing = self.db.scalar(select(ShoppingList).where(ShoppingList.name == clean))
|
||||
if existing:
|
||||
return {"ok": True, "list": self._list_to_dict(existing, include_items=True), "created": False}
|
||||
|
||||
max_order = self.db.scalar(select(func.max(ShoppingList.sort_order))) or 0
|
||||
row = ShoppingList(name=clean, sort_order=max_order + 1)
|
||||
self.db.add(row)
|
||||
self.db.commit()
|
||||
self.db.refresh(row)
|
||||
return {"ok": True, "list": self._list_to_dict(row, include_items=True), "created": True}
|
||||
|
||||
def rename_list(self, list_id: int, name: str) -> dict[str, Any]:
|
||||
row = self._require_list(list_id)
|
||||
clean = name.strip()
|
||||
if not clean:
|
||||
raise ValueError("Название списка не может быть пустым")
|
||||
conflict = self.db.scalar(
|
||||
select(ShoppingList).where(ShoppingList.name == clean, ShoppingList.id != list_id)
|
||||
)
|
||||
if conflict:
|
||||
raise ValueError(f"Список «{clean}» уже существует")
|
||||
row.name = clean
|
||||
row.updated_at = datetime.now(timezone.utc)
|
||||
self.db.commit()
|
||||
self.db.refresh(row)
|
||||
return {"ok": True, "list": self._list_to_dict(row, include_items=True)}
|
||||
|
||||
def delete_list(self, list_id: int) -> dict[str, Any]:
|
||||
row = self._require_list(list_id)
|
||||
name = row.name
|
||||
self.db.delete(row)
|
||||
self.db.commit()
|
||||
return {"ok": True, "deleted_list_id": list_id, "name": name}
|
||||
|
||||
def add_items(
|
||||
self,
|
||||
items: list[dict[str, Any]],
|
||||
*,
|
||||
list_id: int | None = None,
|
||||
list_name: str | None = None,
|
||||
create_list: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
if not items:
|
||||
raise ValueError("Нужен хотя бы один товар")
|
||||
|
||||
row = self._resolve_list(list_id=list_id, name=list_name)
|
||||
if not row and list_name and create_list:
|
||||
created = self.create_list(list_name)
|
||||
row = self._require_list(created["list"]["id"])
|
||||
if not row:
|
||||
raise ValueError("Укажи list_id или list_name")
|
||||
|
||||
max_order = self.db.scalar(
|
||||
select(func.max(ShoppingListItem.sort_order)).where(ShoppingListItem.list_id == row.id)
|
||||
) or 0
|
||||
added: list[dict[str, Any]] = []
|
||||
for idx, raw in enumerate(items, start=1):
|
||||
text = (raw.get("text") or "").strip()
|
||||
if not text:
|
||||
continue
|
||||
item = ShoppingListItem(
|
||||
list_id=row.id,
|
||||
text=text,
|
||||
quantity=raw.get("quantity"),
|
||||
unit=(raw.get("unit") or "").strip(),
|
||||
sort_order=max_order + idx,
|
||||
)
|
||||
self.db.add(item)
|
||||
added.append(item)
|
||||
|
||||
if not added:
|
||||
raise ValueError("Нет валидных товаров для добавления")
|
||||
|
||||
row.updated_at = datetime.now(timezone.utc)
|
||||
self.db.commit()
|
||||
for item in added:
|
||||
self.db.refresh(item)
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"list_id": row.id,
|
||||
"list_name": row.name,
|
||||
"added": [self._item_to_dict(i) for i in added],
|
||||
"list": self._list_to_dict(self._require_list(row.id), include_items=True),
|
||||
}
|
||||
|
||||
def set_item_checked(self, item_id: int, checked: bool) -> dict[str, Any]:
|
||||
item = self._require_item(item_id)
|
||||
item.checked = checked
|
||||
item.shopping_list.updated_at = datetime.now(timezone.utc)
|
||||
self.db.commit()
|
||||
return {"ok": True, "item": self._item_to_dict(item)}
|
||||
|
||||
def remove_item(self, item_id: int) -> dict[str, Any]:
|
||||
item = self._require_item(item_id)
|
||||
data = self._item_to_dict(item)
|
||||
list_id = item.list_id
|
||||
self.db.delete(item)
|
||||
self.db.commit()
|
||||
return {"ok": True, "removed": data, "list_id": list_id}
|
||||
|
||||
def clear_checked(self, list_id: int) -> dict[str, Any]:
|
||||
row = self._require_list(list_id)
|
||||
removed = 0
|
||||
for item in list(row.items):
|
||||
if item.checked:
|
||||
self.db.delete(item)
|
||||
removed += 1
|
||||
row.updated_at = datetime.now(timezone.utc)
|
||||
self.db.commit()
|
||||
return {
|
||||
"ok": True,
|
||||
"list_id": list_id,
|
||||
"removed_count": removed,
|
||||
"list": self._list_to_dict(self._require_list(list_id), include_items=True),
|
||||
}
|
||||
|
||||
def _resolve_list(
|
||||
self,
|
||||
*,
|
||||
list_id: int | None = None,
|
||||
name: str | None = None,
|
||||
) -> ShoppingList | None:
|
||||
if list_id is not None:
|
||||
return self.db.scalar(
|
||||
select(ShoppingList)
|
||||
.where(ShoppingList.id == list_id)
|
||||
.options(selectinload(ShoppingList.items))
|
||||
)
|
||||
if name:
|
||||
clean = name.strip()
|
||||
return self.db.scalar(
|
||||
select(ShoppingList)
|
||||
.where(ShoppingList.name == clean)
|
||||
.options(selectinload(ShoppingList.items))
|
||||
)
|
||||
return None
|
||||
|
||||
def _require_list(self, list_id: int) -> ShoppingList:
|
||||
row = self._resolve_list(list_id=list_id)
|
||||
if not row:
|
||||
raise ValueError(f"Список #{list_id} не найден")
|
||||
return row
|
||||
|
||||
def _require_item(self, item_id: int) -> ShoppingListItem:
|
||||
item = self.db.get(ShoppingListItem, item_id)
|
||||
if not item:
|
||||
raise ValueError(f"Позиция #{item_id} не найдена")
|
||||
return item
|
||||
|
||||
def _item_to_dict(self, item: ShoppingListItem) -> dict[str, Any]:
|
||||
return {
|
||||
"id": item.id,
|
||||
"list_id": item.list_id,
|
||||
"text": item.text,
|
||||
"quantity": item.quantity,
|
||||
"unit": item.unit,
|
||||
"checked": item.checked,
|
||||
"sort_order": item.sort_order,
|
||||
}
|
||||
|
||||
def _list_to_dict(self, row: ShoppingList, *, include_items: bool) -> dict[str, Any]:
|
||||
data: dict[str, Any] = {
|
||||
"id": row.id,
|
||||
"name": row.name,
|
||||
"sort_order": row.sort_order,
|
||||
"item_count": len(row.items) if row.items is not None else 0,
|
||||
"unchecked_count": sum(1 for i in (row.items or []) if not i.checked),
|
||||
}
|
||||
if include_items:
|
||||
data["items"] = [self._item_to_dict(i) for i in (row.items or [])]
|
||||
return data
|
||||
@@ -13,6 +13,7 @@ from app.integrations.wger import WgerClient
|
||||
from app.memory.service import MemoryService
|
||||
from app.pomodoro.service import PomodoroService
|
||||
from app.projects.service import ProjectService
|
||||
from app.shopping.service import ShoppingService
|
||||
|
||||
TOOL_DEFINITIONS: list[dict[str, Any]] = [
|
||||
{
|
||||
@@ -496,6 +497,94 @@ TOOL_DEFINITIONS: list[dict[str, Any]] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "list_shopping_lists",
|
||||
"description": "Все списки покупок с позициями. «Что купить», «покажи списки».",
|
||||
"parameters": {"type": "object", "properties": {}, "required": []},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "create_shopping_list",
|
||||
"description": "Создать новый список покупок.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string", "description": "Название списка, например «Продукты»"},
|
||||
},
|
||||
"required": ["name"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "add_shopping_items",
|
||||
"description": "Добавить товары в список. Список создаётся, если не существует.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"list_name": {"type": "string", "description": "Название списка"},
|
||||
"list_id": {"type": "integer"},
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {"type": "string"},
|
||||
"quantity": {"type": "number"},
|
||||
"unit": {"type": "string"},
|
||||
},
|
||||
"required": ["text"],
|
||||
},
|
||||
},
|
||||
},
|
||||
"required": ["items"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "check_shopping_item",
|
||||
"description": "Отметить позицию как купленную (checked=true) или снять отметку (false).",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"item_id": {"type": "integer"},
|
||||
"checked": {"type": "boolean"},
|
||||
},
|
||||
"required": ["item_id", "checked"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "remove_shopping_item",
|
||||
"description": "Удалить позицию из списка по item_id.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"item_id": {"type": "integer"}},
|
||||
"required": ["item_id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "delete_shopping_list",
|
||||
"description": "Удалить весь список покупок.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"list_id": {"type": "integer"}},
|
||||
"required": ["list_id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
@@ -528,6 +617,7 @@ async def execute_tool(
|
||||
projects = ProjectService(db)
|
||||
memory = MemoryService(db)
|
||||
fitness = FitnessService(db)
|
||||
shopping = ShoppingService(db)
|
||||
|
||||
try:
|
||||
if name == "get_pomodoro_status":
|
||||
@@ -680,6 +770,25 @@ async def execute_tool(
|
||||
draw_self=bool(arguments.get("draw_self")),
|
||||
scene_description=arguments.get("scene_description", ""),
|
||||
)
|
||||
elif name == "list_shopping_lists":
|
||||
result = shopping.list_lists(include_items=True)
|
||||
elif name == "create_shopping_list":
|
||||
result = shopping.create_list(arguments.get("name", ""))
|
||||
elif name == "add_shopping_items":
|
||||
result = shopping.add_items(
|
||||
arguments.get("items") or [],
|
||||
list_id=arguments.get("list_id"),
|
||||
list_name=arguments.get("list_name"),
|
||||
)
|
||||
elif name == "check_shopping_item":
|
||||
result = shopping.set_item_checked(
|
||||
int(arguments["item_id"]),
|
||||
bool(arguments.get("checked", True)),
|
||||
)
|
||||
elif name == "remove_shopping_item":
|
||||
result = shopping.remove_item(int(arguments["item_id"]))
|
||||
elif name == "delete_shopping_list":
|
||||
result = shopping.delete_list(int(arguments["list_id"]))
|
||||
else:
|
||||
return json.dumps({"error": f"Unknown tool: {name}"}, ensure_ascii=False)
|
||||
|
||||
|
||||
@@ -25,6 +25,10 @@
|
||||
- Вопросы о погоде, дожде, «что на улице» — используй get_weather или данные из блока [Погода]
|
||||
- Утренний брифинг — get_morning_briefing
|
||||
|
||||
Списки покупок:
|
||||
- Несколько списков (Продукты, Дом, и т.д.)
|
||||
- add_shopping_items, list_shopping_lists, check_shopping_item
|
||||
|
||||
Картинки:
|
||||
- «Нарисуй себя» → generate_image с draw_self=true
|
||||
- Другая сцена → generate_image с scene_description на английском (booru-теги)
|
||||
|
||||
Reference in New Issue
Block a user