Skip to main content

Build a Support Agent

In this tutorial you will build a customer support agent that remembers customer preferences, searches a knowledge base using semantic similarity, and logs every reasoning step for compliance review.

By the end you will have an agent that:

  • Persists customer interaction history using HatiData memory
  • Searches knowledge base articles with hybrid SQL (semantic_match + JOIN_VECTOR)
  • Records chain-of-thought reasoning to an immutable audit ledger
  • Replays and verifies decision traces on demand

Prerequisites​

  • Python 3.10 or later
  • A HatiData account and API key (sign up)
  • hatidata Python SDK installed
pip install hatidata

Set your credentials as environment variables:

export HATIDATA_API_KEY="hd_your_api_key"
export HATIDATA_ORG="your-org-slug"

Step 1: Set Up HatiData​

Create a client and verify the connection before defining any schema.

import os
from hatidata import HatiDataClient

client = HatiDataClient(
api_key=os.environ["HATIDATA_API_KEY"],
org=os.environ["HATIDATA_ORG"],
)

# Verify connectivity
status = client.ping()
print(f"Connected to HatiData: {status}")

The client handles connection pooling, JWT refresh, and retry logic automatically.


Step 2: Create the Schema​

Define three tables that represent the support agent's data layer: customers, tickets, and a knowledge base.

client.execute("""
CREATE TABLE IF NOT EXISTS customers (
customer_id TEXT PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL,
tier TEXT DEFAULT 'free',
created_at TIMESTAMPTZ DEFAULT now()
)
""")

client.execute("""
CREATE TABLE IF NOT EXISTS support_tickets (
ticket_id TEXT PRIMARY KEY,
customer_id TEXT NOT NULL REFERENCES customers(customer_id),
subject TEXT NOT NULL,
body TEXT NOT NULL,
status TEXT DEFAULT 'open',
resolution TEXT,
created_at TIMESTAMPTZ DEFAULT now(),
resolved_at TIMESTAMPTZ
)
""")

client.execute("""
CREATE TABLE IF NOT EXISTS knowledge_base (
article_id TEXT PRIMARY KEY,
title TEXT NOT NULL,
content TEXT NOT NULL,
category TEXT NOT NULL,
updated_at TIMESTAMPTZ DEFAULT now()
)
""")

print("Schema created.")

Seed a few knowledge base articles for the agent to search against:

articles = [
(
"kb-001",
"How to reset your password",
"Visit /settings/security and click Reset Password. A confirmation link is emailed within 2 minutes.",
"account",
),
(
"kb-002",
"Billing cycle and invoice dates",
"Invoices are generated on the 1st of each month. Enterprise customers receive PDF invoices by email.",
"billing",
),
(
"kb-003",
"API rate limits by tier",
"Free: 100 req/min. Growth: 1000 req/min. Enterprise: unlimited with burst allowance.",
"api",
),
(
"kb-004",
"Data export formats",
"Export your data as Parquet, CSV, or JSON from the dashboard under Settings > Export.",
"data",
),
]

client.executemany(
"INSERT INTO knowledge_base (article_id, title, content, category) VALUES (?, ?, ?, ?)",
articles,
)
print(f"Seeded {len(articles)} knowledge base articles.")

Step 3: Store Customer Memories​

HatiData memory lets the agent persist structured facts about each customer. Memories are stored alongside embeddings so they can be retrieved by semantic similarity later.

from hatidata.memory import MemoryClient

memory = MemoryClient(client)

def remember_interaction(customer_id: str, summary: str, metadata: dict):
"""Store a customer interaction as a searchable memory."""
memory.store(
agent_id="support-agent-v1",
content=summary,
metadata={
"customer_id": customer_id,
"type": "interaction",
**metadata,
},
)

# Store two past interactions for an existing customer
remember_interaction(
customer_id="cust-123",
summary="Customer asked about billing cycle. Prefers email summaries over in-app notifications.",
metadata={"topic": "billing", "sentiment": "neutral"},
)

remember_interaction(
customer_id="cust-123",
summary="Customer reported API rate limit errors during a batch job running at 02:00 UTC.",
metadata={"topic": "api", "sentiment": "frustrated"},
)

print("Customer memories stored.")

Retrieve relevant memories when the agent handles a new ticket:

def recall_customer_context(customer_id: str, query: str) -> list[dict]:
"""Retrieve memories relevant to the current customer query."""
results = memory.search(
agent_id="support-agent-v1",
query=query,
filters={"customer_id": customer_id},
top_k=5,
)
return results

context = recall_customer_context("cust-123", "billing invoice question")
for mem in context:
print(f" [{mem['score']:.2f}] {mem['content']}")

memory.search runs a hybrid query: vector ANN retrieval narrows the candidate set, then the query engine applies the customer_id filter and re-ranks by exact cosine similarity.


Step 4: Search with Hybrid SQL​

HatiData extends SQL with semantic_match() for embedding-based ranking and JOIN_VECTOR for combining semantic scores with structured predicates.

def find_relevant_articles(query: str, category: str | None = None) -> list[dict]:
"""
Search the knowledge base using semantic similarity.
Optionally restrict by category for higher precision.
"""
category_filter = f"AND category = '{category}'" if category else ""

rows = client.query(f"""
SELECT
article_id,
title,
content,
category,
semantic_match(content, '{query}') AS relevance_score
FROM knowledge_base
WHERE semantic_match(content, '{query}') > 0.65
{category_filter}
ORDER BY relevance_score DESC
LIMIT 3
""")

return [dict(row) for row in rows]


def find_articles_with_ticket_context(ticket_body: str) -> list[dict]:
"""
Join knowledge base articles against a ticket using vector similarity.
Returns articles ranked by relevance to the ticket content.
"""
rows = client.query(f"""
SELECT
kb.article_id,
kb.title,
kb.category,
v.similarity
FROM knowledge_base kb
JOIN_VECTOR (
source => '{ticket_body}',
target_column => content,
metric => 'cosine'
) AS v ON kb.article_id = v.row_id
WHERE v.similarity > 0.60
ORDER BY v.similarity DESC
LIMIT 5
""")

return [dict(row) for row in rows]


# Agent handling an incoming billing inquiry
ticket_body = "I have not received my invoice for last month. When do invoices get sent?"

articles = find_articles_with_ticket_context(ticket_body)
print("Relevant articles:")
for a in articles:
print(f" [{a['similarity']:.2f}] {a['title']} (category: {a['category']})")

JOIN_VECTOR lets you combine the semantic ranking with any SQL predicate — for example, restricting to articles updated within the last 90 days.


Step 5: Log Reasoning Steps​

Every decision the agent makes is recorded as a chain-of-thought step in HatiData's immutable ledger. Steps are cryptographically hash-chained, creating a tamper-evident audit trail.

from hatidata.cot import CotClient, StepType

cot = CotClient(client)

def handle_ticket(ticket_id: str, customer_id: str, ticket_body: str) -> str:
"""
Handle a support ticket with full chain-of-thought logging.
Returns the drafted resolution.
"""
session_id = f"ticket-{ticket_id}"

# Observation: incoming ticket
cot.log_step(
agent_id="support-agent-v1",
session_id=session_id,
step_type=StepType.OBSERVATION,
content=f"Received ticket from customer {customer_id}: {ticket_body[:120]}",
metadata={"ticket_id": ticket_id},
)

# Reasoning: retrieve customer context
memories = recall_customer_context(customer_id, ticket_body)
memory_summary = "; ".join(m["content"][:80] for m in memories[:2])

cot.log_step(
agent_id="support-agent-v1",
session_id=session_id,
step_type=StepType.REASONING,
content=f"Retrieved {len(memories)} past interactions. Context: {memory_summary}",
)

# Tool call: knowledge base search
articles = find_relevant_articles(ticket_body)
top_article = articles[0] if articles else None

cot.log_step(
agent_id="support-agent-v1",
session_id=session_id,
step_type=StepType.TOOL_CALL,
content=(
f"Searched knowledge base. Top match: '{top_article['title']}' "
f"(score {top_article['relevance_score']:.2f})"
) if top_article else "No relevant articles found. Escalating.",
metadata={"article_ids": [a["article_id"] for a in articles]},
)

# Conclusion: draft resolution
if top_article:
resolution = (
f"Based on our knowledge base: {top_article['content']} "
"If you need further help, reply to this ticket."
)
else:
resolution = "I have escalated your ticket to a specialist. Expect a response within 4 hours."

cot.log_step(
agent_id="support-agent-v1",
session_id=session_id,
step_type=StepType.CONCLUSION,
content="Resolution drafted. Ticket will be marked resolved.",
metadata={"resolution_length": len(resolution)},
)

# Persist the interaction so future tickets benefit from this context
remember_interaction(
customer_id=customer_id,
summary=(
f"Ticket {ticket_id}: {ticket_body[:80]} — resolved using "
f"article {top_article['article_id'] if top_article else 'escalation'}"
),
metadata={
"ticket_id": ticket_id,
"topic": top_article["category"] if top_article else "unknown",
},
)

return resolution


resolution = handle_ticket(
ticket_id="tkt-9901",
customer_id="cust-123",
ticket_body="I have not received my invoice for last month. When do invoices get sent?",
)
print(f"\nResolution:\n{resolution}")

Step 6: Replay and Verify​

After handling tickets you can replay any session to audit the agent's decisions step by step. HatiData verifies the cryptographic hash chain automatically.

def audit_ticket_session(ticket_id: str):
"""Replay and verify the chain-of-thought trace for a given ticket."""
session_id = f"ticket-{ticket_id}"

trace = cot.replay_session(
agent_id="support-agent-v1",
session_id=session_id,
)

print(f"\nAudit trace for session: {session_id}")
print(f"Steps recorded : {len(trace.steps)}")
print(f"Chain integrity: {'VALID' if trace.chain_valid else 'BROKEN'}")
print()

for i, step in enumerate(trace.steps, 1):
print(f" Step {i} [{step.step_type}] at {step.timestamp}")
print(f" {step.content[:120]}")
print(f" Hash: {step.hash[:16]}...")

audit_ticket_session("tkt-9901")

The chain_valid flag is False if any step was modified after recording. Each step's hash incorporates the previous step's hash, so a single tampered record breaks the entire chain from that point forward.


What You Built​

CapabilityHatiData Feature
Remember customer historyMemoryClient.store() + MemoryClient.search()
Semantic knowledge base searchsemantic_match() SQL function
Structured + vector joinJOIN_VECTOR clause
Immutable reasoning auditCotClient.log_step()
Tamper-evident verificationCotClient.replay_session()

Stay in the loop

Product updates, engineering deep-dives, and agent-native insights. No spam.