Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 110 additions & 61 deletions web/src/app/api/services/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { db } from "@/db";
import { tlsTalos, tlsCommerceServices } from "@/db/schema";
import { and, desc, eq, ilike, lt, ne, or } from "drizzle-orm";
import { parseLimit } from "@/lib/parse-limit";
import { fetchReputations } from "@/lib/reputation-ledger";
import { withTraceContext } from "@/lib/tracing";

// GET /api/services — Discover available services across all TALOS agents
Expand All @@ -16,77 +17,125 @@ async function handleGet(request: NextRequest) {
if (!parsedLimit.ok) return parsedLimit.response;
const limit = parsedLimit.limit;

const conditions = [eq(tlsTalos.status, "Active")];
const minScore = searchParams.has("minScore") ? Number(searchParams.get("minScore")) : undefined;
const minConfidence = searchParams.has("minConfidence") ? Number(searchParams.get("minConfidence")) : undefined;
const allowColdStart = searchParams.get("allowColdStart") === "true";

// Exclude the requesting TALOS's own services
if (selfId) {
conditions.push(ne(tlsCommerceServices.talosId, selfId));
}
let currentCursor = cursor;
const accumulated: any[] = [];
let exhausted = false;

// Filter by TALOS category (case-insensitive match in DB)
if (category) {
conditions.push(ilike(tlsTalos.category, category));
}
// Loop until we fulfill the limit or exhaust the DB
while (accumulated.length < limit && !exhausted) {
const conditions = [eq(tlsTalos.status, "Active")];

// Cursor condition (createdAt DESC with id tiebreaker)
if (cursor) {
const [cursorDate, cursorId] = cursor.split("|");
if (cursorDate && cursorId) {
conditions.push(
or(
lt(tlsCommerceServices.createdAt, new Date(cursorDate)),
and(
eq(tlsCommerceServices.createdAt, new Date(cursorDate)),
lt(tlsCommerceServices.id, cursorId),
),
)!,
);
// Exclude the requesting TALOS's own services
if (selfId) {
conditions.push(ne(tlsCommerceServices.talosId, selfId));
}

// Filter by TALOS category (case-insensitive match in DB)
if (category) {
conditions.push(ilike(tlsTalos.category, category));
}
}

const services = await db
.select({
id: tlsCommerceServices.id,
talosId: tlsCommerceServices.talosId,
talosName: tlsTalos.name,
talosCategory: tlsTalos.category,
serviceName: tlsCommerceServices.serviceName,
description: tlsCommerceServices.description,
price: tlsCommerceServices.price,
currency: tlsCommerceServices.currency,
chains: tlsCommerceServices.chains,
createdAt: tlsCommerceServices.createdAt,
})
.from(tlsCommerceServices)
.innerJoin(tlsTalos, eq(tlsCommerceServices.talosId, tlsTalos.id))
.where(conditions.length > 0 ? and(...conditions) : undefined)
.orderBy(desc(tlsCommerceServices.createdAt), desc(tlsCommerceServices.id))
.limit(limit + 1);

const hasMore = services.length > limit;
const page = hasMore ? services.slice(0, limit) : services;

const results = page.map((s) => ({
talosId: s.talosId,
talosName: s.talosName,
talosCategory: s.talosCategory,
serviceName: s.serviceName,
description: s.description,
price: Number(s.price),
currency: s.currency,
chains: s.chains,
}));
// Cursor condition (createdAt DESC with id tiebreaker)
if (currentCursor) {
const [cursorDate, cursorId] = currentCursor.split("|");
if (cursorDate && cursorId) {
conditions.push(
or(
lt(tlsCommerceServices.createdAt, new Date(cursorDate)),
and(
eq(tlsCommerceServices.createdAt, new Date(cursorDate)),
lt(tlsCommerceServices.id, cursorId),
),
)!,
);
}
}

const services = await db
.select({
id: tlsCommerceServices.id,
talosId: tlsCommerceServices.talosId,
talosName: tlsTalos.name,
talosCategory: tlsTalos.category,
serviceName: tlsCommerceServices.serviceName,
description: tlsCommerceServices.description,
price: tlsCommerceServices.price,
currency: tlsCommerceServices.currency,
chains: tlsCommerceServices.chains,
createdAt: tlsCommerceServices.createdAt,
})
.from(tlsCommerceServices)
.innerJoin(tlsTalos, eq(tlsCommerceServices.talosId, tlsTalos.id))
.where(conditions.length > 0 ? and(...conditions) : undefined)
.orderBy(desc(tlsCommerceServices.createdAt), desc(tlsCommerceServices.id))
.limit(limit * 2);

if (services.length === 0) {
exhausted = true;
break;
}

if (services.length < limit * 2) {
exhausted = true;
}

const talosIds = Array.from(new Set(services.map((s) => s.talosId)));
const reputations = await fetchReputations(talosIds, new Date());

for (const service of services) {
let valid = true;

if (minScore !== undefined || minConfidence !== undefined || allowColdStart) {
const rep = reputations.get(service.talosId);
if (rep) {
if (rep.evidence === "insufficient") {
if (!allowColdStart) valid = false;
} else {
if (minScore !== undefined && rep.score < minScore) valid = false;
if (minConfidence !== undefined && rep.confidence < minConfidence) valid = false;
}
} else {
// Cold start
if (!allowColdStart) valid = false;
}
}

if (valid) {
accumulated.push({
id: service.id,
talosId: service.talosId,
talosName: service.talosName,
talosCategory: service.talosCategory,
serviceName: service.serviceName,
description: service.description,
price: Number(service.price),
currency: service.currency,
chains: service.chains,
createdAt: service.createdAt,
});
if (accumulated.length === limit) {
currentCursor = `${service.createdAt.toISOString()}|${service.id}`;
break;
}
}
currentCursor = `${service.createdAt.toISOString()}|${service.id}`;
}
}

// Shuffle for diversity — agents see different services each cycle
for (let i = results.length - 1; i > 0; i--) {
for (let i = accumulated.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[results[i], results[j]] = [results[j], results[i]];
[accumulated[i], accumulated[j]] = [accumulated[j], accumulated[i]];
}

const lastItem = page[page.length - 1];
const nextCursor = hasMore && lastItem
? `${lastItem.createdAt.toISOString()}|${lastItem.id}`
: null;
const nextCursor = (exhausted && accumulated.length < limit) ? null : currentCursor;

// Remove cursor tracking fields from the output to match original payload structure
const results = accumulated.map(({ id, createdAt, ...rest }) => rest);

return Response.json({ data: results, nextCursor });
} catch {
Expand Down
7 changes: 1 addition & 6 deletions web/src/app/api/talos/[id]/reputation/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
REPUTATION_SCORE_VERSION,
reputationInputsSchema,
ReputationJobInput,
MAX_JOB_AGE_DAYS,
} from "@/lib/reputation";

export const dynamic = "force-dynamic";
Expand Down Expand Up @@ -39,12 +40,6 @@ const querySchema = z.object({
jobLimit: z.coerce.number().int().positive().max(10_000).optional(),
});

/**
* Maximum age of a job (days) that will be considered for scoring.
* Prevents ancient activity from skewing the score and bounds query
* cost at the DB layer (one indexed scan filtered by `createdAt`).
*/
const MAX_JOB_AGE_DAYS = 365;

/**
* GET /api/talos/:id/reputation
Expand Down
Loading
Loading