# 06 — Tool Sistemi

## Ne İş Yapar?

LLM'in "şunu yap" dediğinde gerçek işlemleri gerçekleştiren fonksiyonlardır. Her tool: tanım (Gemini'ye gönderilir), execute fonksiyonu ve opsiyonel onay adımından oluşur.

---

## Tool Listesi

| Tool | Onay Gerekir mi? | Açıklama |
|---|---|---|
| `search_products` | ❌ | Ürün arama + carousel |
| `add_to_cart` | ❌ | Sepete ürün ekle |
| `create_pay_link` | ✅ | PayTR ödeme linki oluştur |
| `collect_lead` | ❌ | Ad/tel/email al → CRM |
| `escalate_to_human` | ❌ | Operatöre devret |

---

## Tool Interface

```python
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any

@dataclass
class ToolResult:
    status: str          # success / error
    data: dict
    error: str | None = None
    ui_blocks: list = None

    @classmethod
    def success(cls, data: dict, ui_blocks: list = None):
        return cls(status="success", data=data, ui_blocks=ui_blocks or [])

    @classmethod
    def error(cls, message: str):
        return cls(status="error", data={}, error=message)


class ToolBase(ABC):
    requires_confirmation: bool = False

    @property
    @abstractmethod
    def name(self) -> str: ...

    @property
    @abstractmethod
    def description(self) -> str: ...

    @property
    @abstractmethod
    def parameters(self) -> dict: ...  # JSON Schema

    @abstractmethod
    async def execute(self, args: dict, ctx: "FlovyContext") -> ToolResult: ...
```

---

## search_products

```python
class SearchProductsTool(ToolBase):
    name = "search_products"
    description = "Tenant'ın ürünlerini arar. Sonuçlar carousel olarak gösterilir."
    parameters = {
        "type": "object",
        "properties": {
            "query": {"type": "string", "description": "Arama metni"},
            "category": {"type": "string"},
            "max_price": {"type": "number"},
            "on_sale": {"type": "boolean"},
            "in_stock_only": {"type": "boolean"},
        }
    }

    async def execute(self, args, ctx) -> ToolResult:
        products = await ProductService.search(
            tenant_id=ctx.tenant_id,
            query=args.get("query", ""),
            category=args.get("category"),
            max_price=args.get("max_price"),
            on_sale=args.get("on_sale", False),
            in_stock_only=args.get("in_stock_only", False),
            limit=8,
        )

        if not products:
            return ToolResult.success({"count": 0}, ui_blocks=[])

        return ToolResult.success(
            {"count": len(products)},
            ui_blocks=[{
                "type": "product_carousel",
                "products": [serialize_product(p) for p in products],
            }]
        )
```

---

## add_to_cart

Session metadata'daki `cart` dict'ini günceller. DB'ye order kaydı oluşturmaz — bu checkout'ta olur.

```python
class AddToCartTool(ToolBase):
    name = "add_to_cart"
    parameters = {
        "type": "object",
        "required": ["product_id", "quantity"],
        "properties": {
            "product_id": {"type": "string"},
            "variant_id": {"type": "string"},
            "quantity": {"type": "integer", "minimum": 1},
        }
    }

    async def execute(self, args, ctx) -> ToolResult:
        product = await ProductService.get(args["product_id"], tenant_id=ctx.tenant_id)
        if not product:
            return ToolResult.error("Ürün bulunamadı.")
        if product.stock_status == "out_of_stock":
            return ToolResult.error(f"{product.title} stokta yok.")
        if product.has_variants and not args.get("variant_id"):
            return ToolResult.error("Lütfen ürün seçeneklerini belirtin (renk/beden vb.)")

        # Session metadata'yı güncelle
        cart = ctx.session.metadata.get("cart", {"items": [], "total": 0.0})
        # ... item ekleme mantığı ...
        ctx.session.metadata["cart"] = cart
        await db.commit()

        return ToolResult.success(
            {"cart": cart, "added": {"title": product.title, "quantity": args["quantity"]}},
            ui_blocks=[{"type": "cart", "cart": cart}]
        )
```

---

## create_pay_link

**Onay gerektiren tool.** LLM önce ziyaretçiye sorar, onay gelince FlovyToolCall(unconfirmed) oluşturulur, widget "Onaylıyor musunuz?" UI'ı gösterir, ziyaretçi onaylayınca `/api/widget/confirm-tool` çağrılır.

```python
class CreatePayLinkTool(ToolBase):
    name = "create_pay_link"
    requires_confirmation = True
    parameters = {
        "type": "object",
        "required": ["amount", "currency"],
        "properties": {
            "amount": {"type": "number"},
            "currency": {"type": "string", "default": "TRY"},
            "description": {"type": "string"},
        }
    }

    async def execute(self, args, ctx) -> ToolResult:
        result = await PayTRService.create_widget_payment_link(
            tenant_id=ctx.tenant_id,
            session_id=ctx.session_id,
            amount=args["amount"],
            currency=args.get("currency", "TRY"),
            email=ctx.session.visitor_email or "widget@flovy.app",
            description=args.get("description", "Flovy Satış"),
        )
        if not result["success"]:
            return ToolResult.error(result["message"])

        # PaymentLink kaydı oluştur (callback için şart)
        await PaymentLinkRepo.create(
            tenant_id=ctx.tenant_id,
            session_id=ctx.session_id,
            paytr_link_id=result["link_id"],
            paytr_link_url=result["link_url"],
            callback_id=result["callback_id"],
            amount=args["amount"],
        )

        return ToolResult.success(
            {"link_url": result["link_url"], "amount": args["amount"], "currency": args.get("currency", "TRY")},
            ui_blocks=[{"type": "paylink", "url": result["link_url"], "amount": args["amount"]}]
        )
```

---

## collect_lead

```python
class CollectLeadTool(ToolBase):
    name = "collect_lead"
    parameters = {
        "type": "object",
        "properties": {
            "name": {"type": "string"},
            "email": {"type": "string"},
            "phone": {"type": "string"},
            "note": {"type": "string"},
        }
    }

    async def execute(self, args, ctx) -> ToolResult:
        # Visitor profilini güncelle (email/phone geldi → merge)
        await VisitorService.merge_identity(
            visitor_id=ctx.visitor_id,
            email=args.get("email"),
            phone=args.get("phone"),
            name=args.get("name"),
        )

        # Lead kaydı
        await LeadRepo.create(
            tenant_id=ctx.tenant_id,
            session_id=ctx.session_id,
            visitor_id=ctx.visitor_id,
            **args,
        )

        return ToolResult.success({"message": "Lead kaydedildi."})
```

---

## FlovyContext

Her tool execute'da taşınan context nesnesi:

```python
@dataclass
class FlovyContext:
    tenant: Tenant
    widget: ChatWidget
    session: ChatSession
    visitor: Visitor | None
    trigger_message: ChatMessage | None = None

    @property
    def tenant_id(self) -> str:
        return str(self.tenant.id)

    @property
    def session_id(self) -> str:
        return str(self.session.id)

    @property
    def visitor_id(self) -> str | None:
        return str(self.visitor.id) if self.visitor else None
```

---

## Tool Confirmation Akışı (Onay Gerektiren)

```
1. LLM create_pay_link çağırır
2. FlovyService:
   a. requires_confirmation=True → execute etme
   b. FlovyToolCall.create(status="unconfirmed", args={...})
   c. UI block: {"type": "confirm", "pending_call_id": ..., "summary": "₺299 ödeme linki oluşturulsun mu?"}
3. Ziyaretçi "Evet" butonuna basar
4. POST /api/widget/confirm-tool { session_token, pending_call_id }
5. Backend:
   a. FlovyToolCall WHERE id=X AND status="unconfirmed" UPDATE status="executing"  ← atomik
   b. 0 satır etkilendiyse → zaten çalıştırıldı, 409 dön
   c. tool.execute(pending.args, ctx)
   d. FlovyToolCall.update(status=result.status, result=...)
6. Ödeme linki UI'a gönderilir
```

---

## Gotcha'lar

- `add_to_cart`: Varyantlı ürünlerde `variant_id` zorunlu — LLM persona'sına yazılmış.
- `create_pay_link`: PaymentLink kaydı oluşturulamazsa 502 dön, link URL'si ziyaretçiye verilmez. Callback'siz link → "ödeme alındı" mesajı düşmez.
- `collect_lead`: Email/telefon gelince `VisitorService.merge_identity` zorunlu — bu olmadan ziyaretçi hafızası anonim kalır.
- Tool execute atomik status güncellemesi: `WHERE status="unconfirmed" UPDATE status="executing"` → 0 satır = başkası aldı → 409.
