<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Veeksha Shree | Agent]]></title><description><![CDATA[Veeksha Shree | Agent]]></description><link>https://veekshashree.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>Veeksha Shree | Agent</title><link>https://veekshashree.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Fri, 16 Oct 2026 08:32:42 GMT</lastBuildDate><atom:link href="https://veekshashree.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[I Built CivicBridge to Remember Civic Complaints With Hindsight]]></title><description><![CDATA[A drain can be cleaned on Monday, marked RESOLVED on Tuesday, and be overflowing again on Friday. If the system treats Friday as a brand-new ticket, it has remembered the status but forgotten the prob]]></description><link>https://veekshashree.hashnode.dev/i-built-civicbridge-to-remember-civic-complaints-with-hindsight</link><guid isPermaLink="true">https://veekshashree.hashnode.dev/i-built-civicbridge-to-remember-civic-complaints-with-hindsight</guid><category><![CDATA[agent-memory]]></category><dc:creator><![CDATA[Veekshashree Bijjam]]></dc:creator><pubDate>Tue, 29 Sep 2026 13:04:41 GMT</pubDate><content:encoded><![CDATA[<p>A drain can be cleaned on Monday, marked RESOLVED on Tuesday, and be overflowing again on Friday. If the system treats Friday as a brand-new ticket, it has remembered the status but forgotten the problem.</p>
<p>That was the failure mode I wanted to eliminate in CivicBridge. The difficult part was not accepting a complaint; it was carrying useful context from one civic event into the next.</p>
<p>I built CivicBridge around that distinction. The application still uses a relational database for transactional state, but I added Hindsight as a persistent memory layer so the system can retain events, recall relevant history, and reflect on recurring patterns instead of treating every request as a fresh conversation.</p>
<h2>What CivicBridge actually does</h2>
<img src="https://cdn.hashnode.com/uploads/covers/6abb5bcc161622f60a724258/42c62717-1839-4874-8ec6-1ca31237be82.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/6abb5bcc161622f60a724258/856e740c-0735-4209-818c-f0be26b7181a.png" alt="" style="display:block;margin:0 auto" />

<p>CivicBridge is a two-sided civic grievance system. A citizen can submit a problem using text or voice, attach evidence, and provide a location. The backend validates the report, extracts structured fields such as category, severity, duration, and location, and decides whether the complaint is ready to move into the government workflow.</p>
<p>A verified complaint can then be grouped with related reports. The government side works with issue clusters rather than a flat list of tickets: authorities can assign a department, publish a response, mark an issue resolved, and receive a citizen confirmation or reopening signal.</p>
<p>The application is split into a Next.js frontend, a FastAPI backend, a relational persistence layer, an AI understanding service, a clustering service, and a dedicated Hindsight service.</p>
<p>The important boundary is between application state and agent memory.</p>
<p>The relational database owns facts such as users, complaints, issue clusters, department actions, statuses, and audit records. Hindsight stores contextual events that help the AI reason about what has happened before.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6abb5bcc161622f60a724258/9ef53cca-7d37-4304-840e-cdfa9837b986.jpg" alt="" style="display:block;margin:0 auto" />

<p>The architecture is intentionally asymmetric: SQL is authoritative for current state, while Hindsight supplies historical context to the parts of the system that need it.</p>
<p>That separation matters. I did not want to replace SQL with a vector database. I wanted memory to answer a different question:</p>
<blockquote>
<p>"What relevant experience should this agent remember right now?"</p>
</blockquote>
<p>For the memory layer, I used <a href="https://github.com/vectorize-io/hindsight">Hindsight on GitHub</a> and its <a href="https://hindsight.vectorize.io/">agent memory documentation</a>.</p>
<h2>The core problem: ticket amnesia</h2>
<p>Consider a simple sequence.</p>
<p>A citizen reports:</p>
<blockquote>
<p>"The drain near Government School has been overflowing for three days."</p>
</blockquote>
<p>The complaint is verified and routed to the drainage department. An authority marks the issue resolved with a note such as "drain cleaned and desilted."</p>
<p>Three days later, another citizen reports water logging at the same location.</p>
<p>A traditional ticket system can store all of this perfectly and still miss the important fact: the second report may be evidence that the first resolution did not address the underlying problem.</p>
<p>That is the core design decision behind CivicBridge.</p>
<p>I retain significant events, not just the initial complaint. A complaint can generate memories for creation, classification, clustering, assignment, authority responses, resolution, reopening, and recurrence.</p>
<p>The memory service centralizes those operations:</p>
<pre><code class="language-python">class HindsightService:
    """
    Centralized Hindsight Persistent Agent Memory Service.
    Wraps official Hindsight SDK client with Retain, Recall, and Reflect operations.
    """

    def __init__(self):
        self.base_url = settings.HINDSIGHT_BASE_URL
        self.api_key = settings.HINDSIGHT_API_KEY
        self.bank_id = settings.HINDSIGHT_BANK_ID or "civic_metro_zone1"
        self.resilient_store = LocalResilientMemoryStore()
        self.client = None
        self.is_live_connected = False
        self._init_client()
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6abb5bcc161622f60a724258/8b6be494-bf70-43af-99e6-dad6939f2997.jpg" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/6abb5bcc161622f60a724258/4b99a23b-b040-4234-a4b2-3391efddca80.jpg" alt="" style="display:block;margin:0 auto" />

<p>This became one of the most useful architectural choices in the project: every part of the application can call the same memory service instead of embedding memory logic inside individual API handlers.</p>
<h2>RETAIN: remembering the right events</h2>
<p>The first step is deciding what is worth remembering.</p>
<p>I do not send raw application state indiscriminately into Hindsight. CivicBridge creates concise memory units with an event type, category, location, status, and metadata.</p>
<p>For example, when a complaint is accepted, the API records a user-specific memory and a municipal memory:</p>
<pre><code class="language-python">user_mem = await hindsight_service.retain_user_memory(
    user_id=current_user.id,
    content=(
        f"User reported {complaint.category.lower()} issue "
        f"({complaint.complaint_code}) at {complaint.location_text}: "
        f"'{complaint.raw_text}'. Verification: {v_status}. "
        f"Routed to {complaint.department}."
    ),
    event_type="complaint_created" if v_status == "VERIFIED"
        else "complaint_verification_held",
    category=complaint.category,
    location=complaint.location_text,
    complaint_id=complaint.id,
)
</code></pre>
<p>The distinction between user memory and municipal memory is deliberate.</p>
<p>A citizen should be able to get continuity across their own sessions without exposing another citizen's history. At the same time, municipal issue clustering needs a broader view of related incidents in an area.</p>
<p>That leads to separate memory banks.</p>
<p>The service normalizes a user ID into a dedicated bank identifier:</p>
<pre><code class="language-python">def get_user_bank_id(user_id):
    raw = str(user_id).strip().lower().replace(" ", "_")
    if raw.startswith("cb_user_"):
        return raw
    if raw.startswith("cb_"):
        return f"cb_user_{raw[3:]}"
    if raw.startswith("user_"):
        return f"cb_{raw}"
    return f"cb_user_{raw}"
</code></pre>
<p>This also gives the system a clear security boundary: user-specific recalls are executed against that user's bank rather than against a global memory stream.</p>
<h2>RECALL: memory has to change behavior</h2>
<p>Storing memories is easy. The harder part is making them useful.</p>
<p>CivicBridge calls Hindsight before clustering a new complaint. The clustering service first recalls related historical context and then combines that evidence with database queries.</p>
<pre><code class="language-python">recall_res = await hindsight_service.recall_related_issues(
    query=complaint.raw_text,
    location=complaint.location_text,
    category=complaint.category,
    limit=10,
)

candidate_clusters = db.query(IssueCluster).filter(
    IssueCluster.category == complaint.category
).all()
</code></pre>
<p>The cluster matcher then evaluates several signals rather than relying on one similarity score.</p>
<p>Category must match. Location overlap contributes evidence. Coordinates can add geographic proximity. Problem language provides topic overlap. Hindsight can add another contextual link when it recalls the same historical cluster.</p>
<p>The resulting score is bounded and combined with a threshold before a complaint is attached to an existing cluster.</p>
<p>This is important because memory is not supposed to make every complaint "sound similar." It is evidence that participates in a deterministic decision process.</p>
<h2>The interesting part: resolving an issue is not the end</h2>
<p>The most useful behavior in CivicBridge appears when an issue comes back after resolution.</p>
<p>When an authority resolves a cluster, CivicBridge retains the resolution in both the municipal memory context and the affected citizen's private bank.</p>
<p>Later, if the issue is matched to a previously resolved cluster, the cluster is reopened and marked recurring:</p>
<pre><code class="language-python">if best_cluster.status == "RESOLVED":
    best_cluster.status = "REOPENED"
    best_cluster.is_recurring = True
    best_cluster.recurrence_count += 1

    await hindsight_service.retain_event(
        event_type="recurring_issue_detected",
        content=(
            f"Recurring civic issue detected for cluster "
            f"{best_cluster.cluster_code} ({best_cluster.title}). "
            f"Previous resolution failed or problem re-emerged at "
            f"{best_cluster.location_text}."
        ),
        metadata={
            "cluster_id": best_cluster.id,
            "location": best_cluster.location_text,
            "category": best_cluster.category,
            "status": "REOPENED",
        },
    )
</code></pre>
<p>This is where the original architectural idea becomes visible in the product.</p>
<h3>Before: every report starts from zero</h3>
<p>A citizen reports a drain overflow. The issue is resolved. A later report arrives at the same location.</p>
<p>Without persistent contextual memory, the second interaction can look like:</p>
<pre><code class="language-text">New complaint → classify → assign → resolve
</code></pre>
<p>The earlier resolution is stored somewhere in the system, but it is not necessarily part of the reasoning context for the new report.</p>
<h3>After: the new report carries history</h3>
<p>With Hindsight, the flow becomes:</p>
<pre><code class="language-text">New complaint
    ↓
Recall related history
    ↓
Previous resolution found
    ↓
Current report matches the old issue
    ↓
Cluster reopened as recurring
</code></pre>
<p>The agent is no longer asking only, "What did the citizen just report?"</p>
<p>It can ask, "What happened here before?"</p>
<p>That historical context can expose a failed or temporary resolution, which is materially different from handling an issue as a new ticket.</p>
<h2>REFLECT: moving from events to patterns</h2>
<p>Recall answers a narrow question: what relevant memories are associated with this context?</p>
<p>Reflection is useful for a broader one: what pattern emerges from those memories?</p>
<p>CivicBridge exposes a reflection path for issue history. The memory service gathers recalled events and synthesizes information such as previous resolutions, authority actions, related reports, and recurrence signals.</p>
<p>The local fallback implementation makes that idea explicit:</p>
<pre><code class="language-python">recalled = self.recall(query=query, bank_id=bank_id, limit=15)

complaints = [
    m for m in recalled
    if m.event_type == "complaint_created"
]
resolutions = [
    m for m in recalled
    if m.event_type == "issue_resolved"
]
reopenings = [
    m for m in recalled
    if m.event_type in ["issue_reopened", "recurring_issue_detected"]
]
</code></pre>
<p>The result is not just a retrieved list. It is a compact historical interpretation.</p>
<p>For a municipal operator, that difference matters. Ten disconnected events are data. "This location has repeated drainage reports and a prior resolution" is operational context.</p>
<h2>Why I kept SQL beside Hindsight</h2>
<p>One of the easiest architectural mistakes here would have been to make Hindsight responsible for everything.</p>
<p>I did not.</p>
<p>SQL remains the source of truth for transactional application state. The database knows whether a complaint is RESOLVED, which department owns it, who filed it, what its cluster ID is, and what actions were recorded.</p>
<p>Hindsight handles contextual memory.</p>
<p>That means I can query the application deterministically:</p>
<pre><code class="language-python">clusters = query.order_by(
    IssueCluster.impact_score.desc(),
    IssueCluster.updated_at.desc()
).all()
</code></pre>
<p>and separately ask memory for historical context around a cluster.</p>
<p>This separation also makes the system easier to reason about. A change to complaint status is a database operation. Learning that similar complaints previously appeared at the same location is a memory operation. The two can inform one another without becoming the same storage problem.</p>
<p>For readers working through the broader agent-memory problem, I found this distinction useful: <a href="https://vectorize.io/what-is-agent-memory">understanding agent memory with Vectorize</a> helped frame memory as a behavioral capability rather than just another persistence mechanism.</p>
<h2>Evidence verification comes before memory-driven routing</h2>
<p>Another design choice I kept in the backend is that memory does not bypass verification.</p>
<p>CivicBridge runs an AI understanding pipeline over the complaint text, evidence, and location. Only a complaint that passes the verification gate is forwarded into the formal government workflow and clustering path.</p>
<p>The API makes that explicit:</p>
<pre><code class="language-python">if not understanding.is_civic_issue \
        or understanding.verification_status == "REJECTED":
    raise HTTPException(
        status_code=status.HTTP_400_BAD_REQUEST,
        detail="This submission does not appear to describe a supported "
               "civic or municipal issue, so it cannot be forwarded."
    )
</code></pre>
<p>If the report is verified, it can be submitted onward. If it needs more evidence, the system holds it rather than pretending certainty.</p>
<p>That ordering is important for an agent that sits between citizens and government systems: memory should improve context, not become a shortcut around validation.</p>
<h2>What the system looks like end to end</h2>
<p>The resulting flow is roughly:</p>
<ol>
<li><p>A citizen submits a civic report.</p>
</li>
<li><p>The AI understanding layer extracts structured information and evaluates evidence.</p>
</li>
<li><p>A verified complaint is persisted in SQL.</p>
</li>
<li><p>CivicBridge retains the relevant event in Hindsight.</p>
</li>
<li><p>The clustering service recalls historical context.</p>
</li>
<li><p>The complaint is attached to an existing issue or a new cluster.</p>
</li>
<li><p>Authorities assign and update the issue.</p>
</li>
<li><p>Responses and resolutions become new memories.</p>
</li>
<li><p>A later report can recall the prior resolution.</p>
</li>
<li><p>If the issue has returned, the cluster can be reopened and marked recurring.</p>
</li>
<li><p>Reflection summarizes the accumulated history for the operator.</p>
</li>
</ol>
<p>The key architectural property is that memory is part of the control loop.</p>
<p>It is not a dashboard ornament and it is not only there to make the assistant sound conversational.</p>
<h2>What I learned</h2>
<h3>1. Memory should be event-oriented, not transcript-oriented</h3>
<p>A raw conversation log is difficult to use operationally. CivicBridge retains meaningful events such as <code>issue_resolved</code>, <code>authority_response</code>, and <code>issue_reopened</code>. Those events are much easier to query and reason over later.</p>
<h3>2. Memory and transactional state solve different problems</h3>
<p>I initially found it tempting to think of Hindsight as another database. It is more useful to treat it as contextual memory sitting beside the database. SQL provides exact state. Hindsight provides relevant experience.</p>
<h3>3. Recall should influence deterministic logic</h3>
<p>A memory system becomes far more useful when its output participates in a concrete decision. In CivicBridge, recalled history contributes to complaint clustering and recurrence detection rather than merely being displayed to a user.</p>
<h3>4. Multi-user memory requires an explicit isolation model</h3>
<p>User-specific banks are not an implementation detail. They are part of the data model. The application needs a clear rule for which memory a request is allowed to retrieve.</p>
<h3>5. The fallback was useful, but it is also a limitation</h3>
<p>The Hindsight service includes a local resilient store and can continue operating when the remote memory service cannot be reached. That is useful for availability, but it is not equivalent to the remote memory system.</p>
<p>The local implementation uses a simpler scoring strategy based on keyword, locality, category, event type, and recency. It is therefore a compatibility and resilience layer, not a perfect substitute for Hindsight's retrieval behavior. That distinction matters: I would not present a disconnected fallback as if it produced identical memory quality.</p>
<p>This was one of the clearest lessons from the implementation. Agent memory needs an explicit degradation strategy, and the degraded mode needs to be visible rather than silently treated as identical to the primary system.</p>
<h2>Where this design goes next</h2>
<p>The architecture gives CivicBridge a useful foundation for deeper civic intelligence: stronger multimodal evidence verification, better geographic matching, richer recurring-issue analysis, and more precise municipal recommendations.</p>
<p>But the most important change is simpler.</p>
<p>A civic system should not forget that a problem has already happened just because a new ticket number was generated.</p>
<p>That is the reason I built CivicBridge around persistent memory with Hindsight. The database remembers what happened. The memory layer helps the agent remember why the next report might matter.</p>
]]></content:encoded></item></channel></rss>