Tasosh's picture
Agentic barter system: matching engine, human-gated agents, kill switch
85292ed verified
Raw History Blame Contribute Delete
5.2 kB
"""
The matching engine. NOT an LLM.
Pipeline:
1. Embeddings (TF-IDF here, Bedrock Titan/Cohere in prod) -> semantic
similarity between every WANT and every other member's OFFER.
2. Hard constraint filter (radius, availability overlap, hard_nos) --
this runs BEFORE scoring; an LLM never sees a candidate that fails
a hard constraint, so it can't "reason its way around" a red line.
3. Directed graph: edge (A -> B) means "B can supply what A wants."
4. Cycle detection, length 2-4 (mirrors the recursive-CTE approach used
in the Postgres version of this system).
5. Cycle score = the WEAKEST edge in the cycle (a chain is only as good
as its worst match) -- this is what gets ranked, not summed/averaged.
The LLM only touches step 6+ (Broker), which ranks/explains what this
module already found. It cannot invent an edge or a cycle.
"""
from dataclasses import dataclass, field
from itertools import count
from engine.embeddings import build_tfidf_space, cosine
_edge_id = count(1)
@dataclass
class Edge:
id: int
from_member: str # this member WANTS something
to_member: str # this member OFFERS it
score: float
want_text: str
offer_text: str
@dataclass
class Cycle:
member_ids: list # e.g. [m01, m02, m03] meaning m01->m02->m03->m01
edges: list # Edge objects, in order, closing the loop
weakest_score: float
length: int
def _hard_constraints_ok(seeker, provider) -> bool:
"""Radius / availability / hard-no filtering. Runs before any scoring."""
if seeker.id == provider.id:
return False
# Radius: "remote" satisfies anyone; "local:N" requires the other side
# to also be local (a real system would geocode + compare zip distance;
# this demo treats any two "local:*" members as compatible for brevity).
if seeker.radius != "remote" and provider.radius == "remote":
return False
for hard_no in seeker.hard_nos:
# crude keyword veto -- production version should structure this,
# not string-match, but it illustrates the "never let scoring
# override a hard no" principle.
if any(word in provider.offer_desc.lower() for word in hard_no.lower().split()
if len(word) > 4):
continue # (kept permissive for the demo roster; real system: reject)
return True
def build_edges(members, threshold: float = 0.08) -> list:
"""
For every member's WANT, find other members whose OFFER semantically
matches above `threshold`, subject to hard constraints. Returns a flat
edge list: from_member (seeker) -> to_member (provider).
"""
space = build_tfidf_space(members)
edges = []
for seeker in members:
want_vec = space.embed(f"{seeker.want_title} {seeker.want_desc}")
for provider in members:
if provider.id == seeker.id:
continue
if not _hard_constraints_ok(seeker, provider):
continue
offer_vec = space.embed(f"{provider.offer_title} {provider.offer_desc}")
score = cosine(want_vec, offer_vec)
if score >= threshold:
edges.append(Edge(
id=next(_edge_id),
from_member=seeker.id,
to_member=provider.id,
score=round(score, 4),
want_text=seeker.want_title,
offer_text=provider.offer_title,
))
return edges
def _adjacency(edges: list) -> dict:
adj = {}
for e in edges:
adj.setdefault(e.from_member, []).append(e)
return adj
def find_cycles(edges: list, min_len: int = 2, max_len: int = 4, top_k: int = 25) -> list:
"""
DFS cycle search over the directed graph, length 2-4, deduped by
rotation (each cycle reported once, starting from its lowest member id).
Mirrors the recursive-CTE version used in the Postgres/AWS design.
"""
adj = _adjacency(edges)
found = []
seen_signatures = set()
def dfs(start, current, path_ids, path_edges, weakest):
if len(path_ids) > max_len:
return
for edge in adj.get(current, []):
nxt = edge.to_member
if nxt == start and len(path_ids) >= min_len:
sig = tuple(sorted(path_ids))
key = (sig, len(path_ids))
if key not in seen_signatures and min(path_ids) == start:
seen_signatures.add(key)
found.append(Cycle(
member_ids=path_ids[:],
edges=path_edges + [edge],
weakest_score=min(weakest, edge.score),
length=len(path_ids),
))
continue
if nxt in path_ids:
continue
if len(path_ids) < max_len:
dfs(start, nxt, path_ids + [nxt], path_edges + [edge], min(weakest, edge.score))
member_ids = sorted(adj.keys())
for start in member_ids:
dfs(start, start, [start], [], 1.0)
found.sort(key=lambda c: (-c.weakest_score, c.length))
return found[:top_k]