LLM Agent tools: cover all terminal pages (hunt/portfolio/sniper/listing/inbox/yield/etc.)
Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled
Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled
This commit is contained in:
@ -2,18 +2,28 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
from decimal import Decimal
|
||||||
from typing import Any, Awaitable, Callable, Optional
|
from typing import Any, Awaitable, Callable, Optional
|
||||||
|
|
||||||
from sqlalchemy import and_, func, select
|
from sqlalchemy import and_, case, func, select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.api.tld_prices import get_trending_tlds
|
from app.api.tld_prices import get_trending_tlds
|
||||||
from app.models.auction import DomainAuction
|
from app.models.auction import DomainAuction
|
||||||
from app.models.domain import Domain
|
from app.models.domain import Domain
|
||||||
from app.models.listing import DomainListing, ListingStatus
|
from app.models.listing import DomainListing, ListingStatus
|
||||||
|
from app.models.listing import ListingInquiry, ListingInquiryMessage
|
||||||
|
from app.models.portfolio import PortfolioDomain
|
||||||
|
from app.models.sniper_alert import SniperAlert
|
||||||
from app.models.subscription import Subscription, SubscriptionTier
|
from app.models.subscription import Subscription, SubscriptionTier
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
|
from app.models.yield_domain import YieldDomain, YieldTransaction
|
||||||
from app.services.analyze.service import get_domain_analysis
|
from app.services.analyze.service import get_domain_analysis
|
||||||
|
from app.services.domain_checker import domain_checker
|
||||||
|
from app.services.hunt.brandables import check_domains, generate_cvcvc, generate_cvccv, generate_human
|
||||||
|
from app.services.hunt.trends import fetch_google_trends_daily_rss
|
||||||
|
from app.services.hunt.typos import generate_typos
|
||||||
from app.services.zone_file import get_dropped_domains
|
from app.services.zone_file import get_dropped_domains
|
||||||
|
|
||||||
|
|
||||||
@ -144,6 +154,413 @@ async def tool_get_dashboard_summary(db: AsyncSession, user: User, args: dict[st
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def tool_hunt_trends(db: AsyncSession, user: User, args: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
geo = (args.get("geo") or "US").strip().upper()
|
||||||
|
if len(geo) != 2:
|
||||||
|
geo = "US"
|
||||||
|
items_raw = await fetch_google_trends_daily_rss(geo=geo)
|
||||||
|
return {
|
||||||
|
"geo": geo,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"title": i["title"],
|
||||||
|
"approx_traffic": i.get("approx_traffic"),
|
||||||
|
"published_at": i.get("published_at"),
|
||||||
|
"link": i.get("link"),
|
||||||
|
}
|
||||||
|
for i in items_raw[:50]
|
||||||
|
],
|
||||||
|
"timestamp": datetime.utcnow().isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def tool_hunt_brandables(db: AsyncSession, user: User, args: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
pattern = (args.get("pattern") or "cvcvc").strip().lower()
|
||||||
|
if pattern not in ("cvcvc", "cvccv", "human"):
|
||||||
|
pattern = "cvcvc"
|
||||||
|
|
||||||
|
tlds = args.get("tlds") or ["com"]
|
||||||
|
if not isinstance(tlds, list):
|
||||||
|
tlds = ["com"]
|
||||||
|
tlds = [str(t).lower().lstrip(".") for t in tlds if str(t).strip()]
|
||||||
|
if not tlds:
|
||||||
|
tlds = ["com"]
|
||||||
|
|
||||||
|
max_checks = _clamp_int(args.get("max_checks"), lo=10, hi=80, default=40)
|
||||||
|
limit = _clamp_int(args.get("limit"), lo=1, hi=25, default=15)
|
||||||
|
|
||||||
|
candidates: list[str] = []
|
||||||
|
for _ in range(max_checks):
|
||||||
|
if pattern == "cvcvc":
|
||||||
|
sld = generate_cvcvc()
|
||||||
|
elif pattern == "cvccv":
|
||||||
|
sld = generate_cvccv()
|
||||||
|
else:
|
||||||
|
sld = generate_human()
|
||||||
|
for t in tlds:
|
||||||
|
candidates.append(f"{sld}.{t}")
|
||||||
|
|
||||||
|
checked = await check_domains(candidates, concurrency=30)
|
||||||
|
available = [c for c in checked if c.status == "available"]
|
||||||
|
seen = set()
|
||||||
|
out = []
|
||||||
|
for c in available:
|
||||||
|
if c.domain in seen:
|
||||||
|
continue
|
||||||
|
seen.add(c.domain)
|
||||||
|
out.append({"domain": c.domain, "status": c.status, "is_available": bool(c.is_available)})
|
||||||
|
if len(out) >= limit:
|
||||||
|
break
|
||||||
|
|
||||||
|
return {"pattern": pattern, "tlds": tlds, "items": out, "timestamp": datetime.utcnow().isoformat()}
|
||||||
|
|
||||||
|
|
||||||
|
async def tool_hunt_typos(db: AsyncSession, user: User, args: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
brand = (args.get("brand") or "").strip()
|
||||||
|
if not brand or len(brand) < 2:
|
||||||
|
return {"error": "Missing brand"}
|
||||||
|
limit = _clamp_int(args.get("limit"), lo=1, hi=30, default=15)
|
||||||
|
tlds = args.get("tlds") or ["com"]
|
||||||
|
if not isinstance(tlds, list):
|
||||||
|
tlds = ["com"]
|
||||||
|
tlds = [str(t).lower().lstrip(".") for t in tlds if str(t).strip()]
|
||||||
|
if not tlds:
|
||||||
|
tlds = ["com"]
|
||||||
|
|
||||||
|
# generate_typos returns SLD strings; we format domains for UX
|
||||||
|
slugs = generate_typos(brand, limit=limit)
|
||||||
|
items = []
|
||||||
|
for sld in slugs:
|
||||||
|
for t in tlds[:3]:
|
||||||
|
items.append(f"{sld}.{t}")
|
||||||
|
if len(items) >= limit:
|
||||||
|
break
|
||||||
|
if len(items) >= limit:
|
||||||
|
break
|
||||||
|
|
||||||
|
return {"brand": brand, "items": items, "timestamp": datetime.utcnow().isoformat()}
|
||||||
|
|
||||||
|
|
||||||
|
async def tool_keyword_availability(db: AsyncSession, user: User, args: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
keyword = (args.get("keyword") or "").strip().lower()
|
||||||
|
if not keyword:
|
||||||
|
return {"error": "Missing keyword"}
|
||||||
|
tlds = args.get("tlds") or ["com"]
|
||||||
|
if not isinstance(tlds, list):
|
||||||
|
tlds = ["com"]
|
||||||
|
tlds = [str(t).lower().lstrip(".") for t in tlds if str(t).strip()]
|
||||||
|
if not tlds:
|
||||||
|
tlds = ["com"]
|
||||||
|
limit = _clamp_int(args.get("limit"), lo=1, hi=25, default=10)
|
||||||
|
|
||||||
|
candidates = [f"{keyword}.{t}" for t in tlds][:limit]
|
||||||
|
results = []
|
||||||
|
for d in candidates:
|
||||||
|
r = await domain_checker.check_domain(d)
|
||||||
|
results.append({"domain": d, "status": r.status, "is_available": bool(getattr(r, "is_available", False))})
|
||||||
|
return {"keyword": keyword, "items": results, "timestamp": datetime.utcnow().isoformat()}
|
||||||
|
|
||||||
|
|
||||||
|
async def tool_portfolio_summary(db: AsyncSession, user: User, args: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
rows = (await db.execute(select(PortfolioDomain).where(PortfolioDomain.user_id == user.id))).scalars().all()
|
||||||
|
total = len(rows)
|
||||||
|
active = sum(1 for d in rows if not d.is_sold and (d.status or "active") != "expired")
|
||||||
|
sold = sum(1 for d in rows if bool(d.is_sold))
|
||||||
|
total_cost = float(sum(Decimal(str(d.purchase_price or 0)) for d in rows))
|
||||||
|
total_value = float(sum(Decimal(str(d.estimated_value or 0)) for d in rows if not d.is_sold))
|
||||||
|
roi = None
|
||||||
|
if total_cost > 0:
|
||||||
|
roi = round(((total_value - total_cost) / total_cost) * 100, 2)
|
||||||
|
return {
|
||||||
|
"total_domains": total,
|
||||||
|
"active_domains": active,
|
||||||
|
"sold_domains": sold,
|
||||||
|
"total_cost": round(total_cost, 2),
|
||||||
|
"total_estimated_value": round(total_value, 2),
|
||||||
|
"overall_roi_percent": roi,
|
||||||
|
"timestamp": datetime.utcnow().isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def tool_list_portfolio(db: AsyncSession, user: User, args: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
limit = _clamp_int(args.get("limit"), lo=1, hi=50, default=25)
|
||||||
|
status = (args.get("status") or "").strip().lower() or None
|
||||||
|
q = select(PortfolioDomain).where(PortfolioDomain.user_id == user.id)
|
||||||
|
if status:
|
||||||
|
q = q.where(PortfolioDomain.status == status)
|
||||||
|
q = q.order_by(PortfolioDomain.created_at.desc()).limit(limit)
|
||||||
|
rows = (await db.execute(q)).scalars().all()
|
||||||
|
return {
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": d.id,
|
||||||
|
"domain": d.domain,
|
||||||
|
"status": d.status,
|
||||||
|
"purchase_price": d.purchase_price,
|
||||||
|
"estimated_value": d.estimated_value,
|
||||||
|
"roi": d.roi,
|
||||||
|
"renewal_date": d.renewal_date.isoformat() if d.renewal_date else None,
|
||||||
|
"is_dns_verified": bool(getattr(d, "is_dns_verified", False) or False),
|
||||||
|
}
|
||||||
|
for d in rows
|
||||||
|
],
|
||||||
|
"count": len(rows),
|
||||||
|
"timestamp": datetime.utcnow().isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def tool_list_sniper_alerts(db: AsyncSession, user: User, args: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
limit = _clamp_int(args.get("limit"), lo=1, hi=50, default=25)
|
||||||
|
rows = (
|
||||||
|
await db.execute(
|
||||||
|
select(SniperAlert)
|
||||||
|
.where(SniperAlert.user_id == user.id)
|
||||||
|
.order_by(SniperAlert.created_at.desc())
|
||||||
|
.limit(limit)
|
||||||
|
)
|
||||||
|
).scalars().all()
|
||||||
|
return {
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": a.id,
|
||||||
|
"name": a.name,
|
||||||
|
"description": a.description,
|
||||||
|
"is_active": bool(a.is_active),
|
||||||
|
"tlds": a.tlds,
|
||||||
|
"keywords": a.keywords,
|
||||||
|
"exclude_keywords": a.exclude_keywords,
|
||||||
|
"max_price": a.max_price,
|
||||||
|
"ending_within_hours": a.ending_within_hours,
|
||||||
|
"notify_email": bool(a.notify_email),
|
||||||
|
"notify_sms": bool(a.notify_sms),
|
||||||
|
"created_at": a.created_at.isoformat() if a.created_at else None,
|
||||||
|
}
|
||||||
|
for a in rows
|
||||||
|
],
|
||||||
|
"count": len(rows),
|
||||||
|
"timestamp": datetime.utcnow().isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def tool_list_my_listings(db: AsyncSession, user: User, args: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
limit = _clamp_int(args.get("limit"), lo=1, hi=50, default=25)
|
||||||
|
rows = (
|
||||||
|
await db.execute(
|
||||||
|
select(DomainListing)
|
||||||
|
.where(DomainListing.user_id == user.id)
|
||||||
|
.order_by(DomainListing.updated_at.desc())
|
||||||
|
.limit(limit)
|
||||||
|
)
|
||||||
|
).scalars().all()
|
||||||
|
return {
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": l.id,
|
||||||
|
"domain": l.domain,
|
||||||
|
"status": l.status,
|
||||||
|
"asking_price": l.asking_price,
|
||||||
|
"min_offer": l.min_offer,
|
||||||
|
"currency": l.currency,
|
||||||
|
"slug": l.slug,
|
||||||
|
"verification_status": l.verification_status,
|
||||||
|
"view_count": l.view_count,
|
||||||
|
"inquiry_count": l.inquiry_count,
|
||||||
|
"updated_at": l.updated_at.isoformat() if l.updated_at else None,
|
||||||
|
}
|
||||||
|
for l in rows
|
||||||
|
],
|
||||||
|
"count": len(rows),
|
||||||
|
"timestamp": datetime.utcnow().isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def tool_get_inbox_counts(db: AsyncSession, user: User, args: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
buyer_inqs = (await db.execute(select(ListingInquiry.id).where(ListingInquiry.buyer_user_id == user.id))).scalars().all()
|
||||||
|
buyer_unread = 0
|
||||||
|
for inq_id in list(buyer_inqs)[:200]:
|
||||||
|
msg = (
|
||||||
|
await db.execute(
|
||||||
|
select(ListingInquiryMessage)
|
||||||
|
.where(ListingInquiryMessage.inquiry_id == inq_id)
|
||||||
|
.order_by(ListingInquiryMessage.created_at.desc())
|
||||||
|
.limit(1)
|
||||||
|
)
|
||||||
|
).scalar_one_or_none()
|
||||||
|
if msg and msg.sender_user_id != user.id:
|
||||||
|
buyer_unread += 1
|
||||||
|
|
||||||
|
listing_ids = (await db.execute(select(DomainListing.id).where(DomainListing.user_id == user.id))).scalars().all()
|
||||||
|
seller_unread = 0
|
||||||
|
if listing_ids:
|
||||||
|
new_count = (
|
||||||
|
await db.execute(
|
||||||
|
select(func.count(ListingInquiry.id)).where(
|
||||||
|
and_(ListingInquiry.listing_id.in_(listing_ids), ListingInquiry.status == "new")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).scalar() or 0
|
||||||
|
seller_unread += int(new_count)
|
||||||
|
|
||||||
|
inqs = (
|
||||||
|
await db.execute(
|
||||||
|
select(ListingInquiry.id, ListingInquiry.status)
|
||||||
|
.where(
|
||||||
|
and_(
|
||||||
|
ListingInquiry.listing_id.in_(listing_ids),
|
||||||
|
ListingInquiry.status.notin_(["closed", "spam"]),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.order_by(ListingInquiry.created_at.desc())
|
||||||
|
.limit(200)
|
||||||
|
)
|
||||||
|
).all()
|
||||||
|
for inq_id, st in inqs:
|
||||||
|
if st == "new":
|
||||||
|
continue
|
||||||
|
msg = (
|
||||||
|
await db.execute(
|
||||||
|
select(ListingInquiryMessage)
|
||||||
|
.where(ListingInquiryMessage.inquiry_id == inq_id)
|
||||||
|
.order_by(ListingInquiryMessage.created_at.desc())
|
||||||
|
.limit(1)
|
||||||
|
)
|
||||||
|
).scalar_one_or_none()
|
||||||
|
if msg and msg.sender_user_id != user.id:
|
||||||
|
seller_unread += 1
|
||||||
|
|
||||||
|
return {"buyer_unread": buyer_unread, "seller_unread": seller_unread, "total_unread": buyer_unread + seller_unread}
|
||||||
|
|
||||||
|
|
||||||
|
async def tool_get_seller_inbox(db: AsyncSession, user: User, args: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
limit = _clamp_int(args.get("limit"), lo=1, hi=50, default=25)
|
||||||
|
status_filter = (args.get("status") or "all").strip().lower()
|
||||||
|
|
||||||
|
listings = (await db.execute(select(DomainListing).where(DomainListing.user_id == user.id))).scalars().all()
|
||||||
|
listing_map = {l.id: l for l in listings}
|
||||||
|
if not listing_map:
|
||||||
|
return {"inquiries": [], "total": 0, "unread": 0}
|
||||||
|
|
||||||
|
q = (
|
||||||
|
select(ListingInquiry)
|
||||||
|
.where(ListingInquiry.listing_id.in_(list(listing_map.keys())))
|
||||||
|
.order_by(ListingInquiry.created_at.desc())
|
||||||
|
)
|
||||||
|
if status_filter and status_filter != "all":
|
||||||
|
q = q.where(ListingInquiry.status == status_filter)
|
||||||
|
inqs = (await db.execute(q.limit(limit))).scalars().all()
|
||||||
|
|
||||||
|
unread = sum(1 for i in inqs if i.status == "new" or not i.read_at)
|
||||||
|
items = []
|
||||||
|
for inq in inqs:
|
||||||
|
listing = listing_map.get(inq.listing_id)
|
||||||
|
if not listing:
|
||||||
|
continue
|
||||||
|
latest_msg = (
|
||||||
|
await db.execute(
|
||||||
|
select(ListingInquiryMessage)
|
||||||
|
.where(ListingInquiryMessage.inquiry_id == inq.id)
|
||||||
|
.order_by(ListingInquiryMessage.created_at.desc())
|
||||||
|
.limit(1)
|
||||||
|
)
|
||||||
|
).scalar_one_or_none()
|
||||||
|
items.append(
|
||||||
|
{
|
||||||
|
"id": inq.id,
|
||||||
|
"domain": listing.domain,
|
||||||
|
"listing_id": listing.id,
|
||||||
|
"slug": listing.slug,
|
||||||
|
"status": inq.status,
|
||||||
|
"buyer_name": inq.name,
|
||||||
|
"offer_amount": inq.offer_amount,
|
||||||
|
"created_at": inq.created_at.isoformat() if inq.created_at else None,
|
||||||
|
"has_unread_reply": bool(latest_msg and latest_msg.sender_user_id != user.id and inq.status not in ["closed", "spam"]),
|
||||||
|
"last_message_preview": (
|
||||||
|
(latest_msg.body[:100] + "..." if len(latest_msg.body) > 100 else latest_msg.body)
|
||||||
|
if latest_msg
|
||||||
|
else ((inq.message or "")[:100])
|
||||||
|
),
|
||||||
|
"last_message_at": latest_msg.created_at.isoformat() if latest_msg and latest_msg.created_at else (inq.created_at.isoformat() if inq.created_at else None),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"inquiries": items, "total": len(items), "unread": unread, "timestamp": datetime.utcnow().isoformat()}
|
||||||
|
|
||||||
|
|
||||||
|
async def tool_yield_dashboard(db: AsyncSession, user: User, args: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
now = datetime.utcnow()
|
||||||
|
month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||||
|
|
||||||
|
domains = (
|
||||||
|
await db.execute(
|
||||||
|
select(YieldDomain)
|
||||||
|
.where(YieldDomain.user_id == user.id)
|
||||||
|
.order_by(YieldDomain.total_revenue.desc())
|
||||||
|
)
|
||||||
|
).scalars().all()
|
||||||
|
domain_ids = [d.id for d in domains]
|
||||||
|
|
||||||
|
monthly_revenue = Decimal("0")
|
||||||
|
monthly_clicks = 0
|
||||||
|
monthly_conversions = 0
|
||||||
|
if domain_ids:
|
||||||
|
monthly_stats = (
|
||||||
|
await db.execute(
|
||||||
|
select(
|
||||||
|
func.coalesce(
|
||||||
|
func.sum(
|
||||||
|
case(
|
||||||
|
(YieldTransaction.status.in_(["confirmed", "paid"]), YieldTransaction.net_amount),
|
||||||
|
else_=0,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
0,
|
||||||
|
).label("revenue"),
|
||||||
|
func.sum(case((YieldTransaction.event_type == "click", 1), else_=0)).label("clicks"),
|
||||||
|
func.sum(
|
||||||
|
case(
|
||||||
|
(
|
||||||
|
and_(
|
||||||
|
YieldTransaction.event_type.in_(["lead", "sale"]),
|
||||||
|
YieldTransaction.status.in_(["confirmed", "paid"]),
|
||||||
|
),
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
else_=0,
|
||||||
|
)
|
||||||
|
).label("conversions"),
|
||||||
|
).where(YieldTransaction.yield_domain_id.in_(domain_ids), YieldTransaction.created_at >= month_start)
|
||||||
|
)
|
||||||
|
).first()
|
||||||
|
if monthly_stats:
|
||||||
|
monthly_revenue = monthly_stats.revenue or Decimal("0")
|
||||||
|
monthly_clicks = int(monthly_stats.clicks or 0)
|
||||||
|
monthly_conversions = int(monthly_stats.conversions or 0)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"stats": {
|
||||||
|
"active_domains": sum(1 for d in domains if d.status == "active"),
|
||||||
|
"total_domains": len(domains),
|
||||||
|
"monthly_revenue": float(monthly_revenue),
|
||||||
|
"monthly_clicks": monthly_clicks,
|
||||||
|
"monthly_conversions": monthly_conversions,
|
||||||
|
},
|
||||||
|
"top_domains": [
|
||||||
|
{
|
||||||
|
"id": d.id,
|
||||||
|
"domain": d.domain,
|
||||||
|
"status": d.status,
|
||||||
|
"dns_verified": bool(d.dns_verified),
|
||||||
|
"total_revenue": float(d.total_revenue or 0),
|
||||||
|
"total_clicks": int(d.total_clicks or 0),
|
||||||
|
"total_conversions": int(d.total_conversions or 0),
|
||||||
|
"detected_intent": d.detected_intent,
|
||||||
|
}
|
||||||
|
for d in domains[:10]
|
||||||
|
],
|
||||||
|
"timestamp": now.isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def tool_list_watchlist(db: AsyncSession, user: User, args: dict[str, Any]) -> dict[str, Any]:
|
async def tool_list_watchlist(db: AsyncSession, user: User, args: dict[str, Any]) -> dict[str, Any]:
|
||||||
page = _clamp_int(args.get("page"), lo=1, hi=50, default=1)
|
page = _clamp_int(args.get("page"), lo=1, hi=50, default=1)
|
||||||
per_page = _clamp_int(args.get("per_page"), lo=1, hi=50, default=20)
|
per_page = _clamp_int(args.get("per_page"), lo=1, hi=50, default=20)
|
||||||
@ -278,6 +695,65 @@ def get_tool_defs() -> list[ToolDef]:
|
|||||||
min_tier=SubscriptionTier.TRADER,
|
min_tier=SubscriptionTier.TRADER,
|
||||||
handler=tool_get_dashboard_summary,
|
handler=tool_get_dashboard_summary,
|
||||||
),
|
),
|
||||||
|
ToolDef(
|
||||||
|
name="hunt_trends",
|
||||||
|
description="Get Google Trends daily RSS items (Hunt > Trends).",
|
||||||
|
json_schema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"geo": {"type": "string", "minLength": 2, "maxLength": 2}},
|
||||||
|
"additionalProperties": False,
|
||||||
|
},
|
||||||
|
min_tier=SubscriptionTier.TRADER,
|
||||||
|
handler=tool_hunt_trends,
|
||||||
|
),
|
||||||
|
ToolDef(
|
||||||
|
name="hunt_brandables",
|
||||||
|
description="Generate brandable domains and check availability (Hunt > Forge).",
|
||||||
|
json_schema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"pattern": {"type": "string", "enum": ["cvcvc", "cvccv", "human"]},
|
||||||
|
"tlds": {"type": "array", "items": {"type": "string"}},
|
||||||
|
"max_checks": {"type": "integer", "minimum": 10, "maximum": 80},
|
||||||
|
"limit": {"type": "integer", "minimum": 1, "maximum": 25},
|
||||||
|
},
|
||||||
|
"additionalProperties": False,
|
||||||
|
},
|
||||||
|
min_tier=SubscriptionTier.TRADER,
|
||||||
|
handler=tool_hunt_brandables,
|
||||||
|
),
|
||||||
|
ToolDef(
|
||||||
|
name="hunt_typos",
|
||||||
|
description="Generate typo candidates for a brand (Hunt > Typos).",
|
||||||
|
json_schema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"brand": {"type": "string"},
|
||||||
|
"tlds": {"type": "array", "items": {"type": "string"}},
|
||||||
|
"limit": {"type": "integer", "minimum": 1, "maximum": 30},
|
||||||
|
},
|
||||||
|
"required": ["brand"],
|
||||||
|
"additionalProperties": False,
|
||||||
|
},
|
||||||
|
min_tier=SubscriptionTier.TRADER,
|
||||||
|
handler=tool_hunt_typos,
|
||||||
|
),
|
||||||
|
ToolDef(
|
||||||
|
name="keyword_availability",
|
||||||
|
description="Check availability for keyword + TLD list (Hunt > Search).",
|
||||||
|
json_schema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"keyword": {"type": "string"},
|
||||||
|
"tlds": {"type": "array", "items": {"type": "string"}},
|
||||||
|
"limit": {"type": "integer", "minimum": 1, "maximum": 25},
|
||||||
|
},
|
||||||
|
"required": ["keyword"],
|
||||||
|
"additionalProperties": False,
|
||||||
|
},
|
||||||
|
min_tier=SubscriptionTier.TRADER,
|
||||||
|
handler=tool_keyword_availability,
|
||||||
|
),
|
||||||
ToolDef(
|
ToolDef(
|
||||||
name="list_watchlist",
|
name="list_watchlist",
|
||||||
description="List user's watchlist domains (monitored domains).",
|
description="List user's watchlist domains (monitored domains).",
|
||||||
@ -292,6 +768,70 @@ def get_tool_defs() -> list[ToolDef]:
|
|||||||
min_tier=SubscriptionTier.TRADER,
|
min_tier=SubscriptionTier.TRADER,
|
||||||
handler=tool_list_watchlist,
|
handler=tool_list_watchlist,
|
||||||
),
|
),
|
||||||
|
ToolDef(
|
||||||
|
name="portfolio_summary",
|
||||||
|
description="Get portfolio summary (counts, total cost/value, ROI).",
|
||||||
|
json_schema={"type": "object", "properties": {}, "additionalProperties": False},
|
||||||
|
min_tier=SubscriptionTier.TRADER,
|
||||||
|
handler=tool_portfolio_summary,
|
||||||
|
),
|
||||||
|
ToolDef(
|
||||||
|
name="list_portfolio",
|
||||||
|
description="List portfolio domains (owned domains).",
|
||||||
|
json_schema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {"type": "string"},
|
||||||
|
"limit": {"type": "integer", "minimum": 1, "maximum": 50},
|
||||||
|
},
|
||||||
|
"additionalProperties": False,
|
||||||
|
},
|
||||||
|
min_tier=SubscriptionTier.TRADER,
|
||||||
|
handler=tool_list_portfolio,
|
||||||
|
),
|
||||||
|
ToolDef(
|
||||||
|
name="list_sniper_alerts",
|
||||||
|
description="List sniper alerts (user-defined filters).",
|
||||||
|
json_schema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"limit": {"type": "integer", "minimum": 1, "maximum": 50}},
|
||||||
|
"additionalProperties": False,
|
||||||
|
},
|
||||||
|
min_tier=SubscriptionTier.TRADER,
|
||||||
|
handler=tool_list_sniper_alerts,
|
||||||
|
),
|
||||||
|
ToolDef(
|
||||||
|
name="list_my_listings",
|
||||||
|
description="List seller's Pounce Direct listings (For Sale).",
|
||||||
|
json_schema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"limit": {"type": "integer", "minimum": 1, "maximum": 50}},
|
||||||
|
"additionalProperties": False,
|
||||||
|
},
|
||||||
|
min_tier=SubscriptionTier.TRADER,
|
||||||
|
handler=tool_list_my_listings,
|
||||||
|
),
|
||||||
|
ToolDef(
|
||||||
|
name="get_inbox_counts",
|
||||||
|
description="Get unified buyer/seller unread counts for inbox badge.",
|
||||||
|
json_schema={"type": "object", "properties": {}, "additionalProperties": False},
|
||||||
|
min_tier=SubscriptionTier.TRADER,
|
||||||
|
handler=tool_get_inbox_counts,
|
||||||
|
),
|
||||||
|
ToolDef(
|
||||||
|
name="get_seller_inbox",
|
||||||
|
description="Seller inbox threads across all listings (preview list).",
|
||||||
|
json_schema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {"type": "string", "enum": ["all", "new", "read", "replied", "closed", "spam"]},
|
||||||
|
"limit": {"type": "integer", "minimum": 1, "maximum": 50},
|
||||||
|
},
|
||||||
|
"additionalProperties": False,
|
||||||
|
},
|
||||||
|
min_tier=SubscriptionTier.TRADER,
|
||||||
|
handler=tool_get_seller_inbox,
|
||||||
|
),
|
||||||
ToolDef(
|
ToolDef(
|
||||||
name="analyze_domain",
|
name="analyze_domain",
|
||||||
description="Run Pounce domain analysis (Authority/Market/Risk/Value) for a given domain.",
|
description="Run Pounce domain analysis (Authority/Market/Risk/Value) for a given domain.",
|
||||||
@ -347,6 +887,13 @@ def get_tool_defs() -> list[ToolDef]:
|
|||||||
min_tier=SubscriptionTier.TRADER,
|
min_tier=SubscriptionTier.TRADER,
|
||||||
handler=tool_get_drops,
|
handler=tool_get_drops,
|
||||||
),
|
),
|
||||||
|
ToolDef(
|
||||||
|
name="yield_dashboard",
|
||||||
|
description="Get Yield dashboard stats and top earning domains.",
|
||||||
|
json_schema={"type": "object", "properties": {}, "additionalProperties": False},
|
||||||
|
min_tier=SubscriptionTier.TRADER,
|
||||||
|
handler=tool_yield_dashboard,
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -357,11 +904,38 @@ def tools_for_path(path: str) -> list[str]:
|
|||||||
"""
|
"""
|
||||||
p = (path or "").split("?")[0]
|
p = (path or "").split("?")[0]
|
||||||
if p.startswith("/terminal/hunt"):
|
if p.startswith("/terminal/hunt"):
|
||||||
return ["get_subscription", "get_dashboard_summary", "market_feed", "get_drops", "analyze_domain", "list_watchlist"]
|
return [
|
||||||
|
"get_subscription",
|
||||||
|
"get_dashboard_summary",
|
||||||
|
"market_feed",
|
||||||
|
"get_drops",
|
||||||
|
"hunt_trends",
|
||||||
|
"hunt_brandables",
|
||||||
|
"hunt_typos",
|
||||||
|
"keyword_availability",
|
||||||
|
"analyze_domain",
|
||||||
|
"list_watchlist",
|
||||||
|
]
|
||||||
if p.startswith("/terminal/market"):
|
if p.startswith("/terminal/market"):
|
||||||
return ["get_subscription", "market_feed", "analyze_domain"]
|
return ["get_subscription", "market_feed", "analyze_domain"]
|
||||||
if p.startswith("/terminal/watchlist"):
|
if p.startswith("/terminal/watchlist"):
|
||||||
return ["get_subscription", "list_watchlist", "analyze_domain"]
|
return ["get_subscription", "list_watchlist", "analyze_domain"]
|
||||||
|
if p.startswith("/terminal/portfolio"):
|
||||||
|
return ["get_subscription", "portfolio_summary", "list_portfolio", "analyze_domain"]
|
||||||
|
if p.startswith("/terminal/sniper"):
|
||||||
|
return ["get_subscription", "list_sniper_alerts", "market_feed", "analyze_domain"]
|
||||||
|
if p.startswith("/terminal/listing"):
|
||||||
|
return ["get_subscription", "list_my_listings", "get_seller_inbox"]
|
||||||
|
if p.startswith("/terminal/inbox"):
|
||||||
|
return ["get_subscription", "get_inbox_counts", "get_seller_inbox"]
|
||||||
|
if p.startswith("/terminal/yield"):
|
||||||
|
return ["get_subscription", "yield_dashboard"]
|
||||||
|
if p.startswith("/terminal/intel"):
|
||||||
|
return ["get_subscription", "analyze_domain", "market_feed", "get_drops"]
|
||||||
|
if p.startswith("/terminal/settings"):
|
||||||
|
return ["get_subscription"]
|
||||||
|
if p.startswith("/terminal/welcome"):
|
||||||
|
return ["get_subscription", "get_dashboard_summary"]
|
||||||
# default: allow a safe minimal set
|
# default: allow a safe minimal set
|
||||||
return ["get_subscription", "get_dashboard_summary", "analyze_domain"]
|
return ["get_subscription", "get_dashboard_summary", "analyze_domain"]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user