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
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,8 @@ MAKE_MCP_TOKEN=
GCP_MCP_URL=
DIA_AGENT_MCP_MODE=auto
DIA_AGENT_ALLOW_GCP_MCP_TOOLS=
# Client memory Sheet (optional): the ID of a Google Sheet with `clients` and
# `engagements` tabs — DIA recalls repeat clients and records every scored
# intake there. Auth is ADC (spreadsheets scope); share the Sheet with the
# service-account email when deployed. Leave unset to run memoryless.
MEMORY_SHEET_ID=
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,33 @@ The MCP URL is configuration, not a secret. Authentication uses the Cloud Run
runtime service account through Application Default Credentials, so grant that
service account only the Google Cloud roles the selected MCP endpoint needs.

## Client memory (optional, Sheets-backed)

DIA can remember clients between intakes using a Google Sheet **in your own
Google account** — client data never leaves your Drive and never touches any
shared infrastructure. Design: `docs/2026-07-07_client-memory-layer_design.md`.

Setup:

1. Create a Google Sheet with two tabs named `clients` and `engagements`.
Put these header rows in row 1:
- `clients`: `client_id | company_name | website | industry | first_seen_at | last_seen_at | engagement_count | last_tier | last_total_score | status | updated_at`
- `engagements`: `engagement_id | client_id | timestamp | source | tier | total_score | max_score | pain_points | goals | budget | documents_generated | proposal_outcome | notes`
2. Set `MEMORY_SHEET_ID` to the Sheet's ID (the long segment of its URL).
3. Auth is ADC: locally, `gcloud auth application-default login`; on Cloud
Run, share the Sheet (Editor) with the runtime service account's email.

Behavior: every scored intake appends an `engagements` row (INSERT_ROWS —
safe under concurrent writers) and updates the client's rollup row. The live
agent gains a `recall_client_history` tool and references prior engagements
when a repeat company comes in. `proposal_outcome` and `status` are yours to
edit directly in the Sheet (e.g. mark a proposal `won`) — human-in-the-loop
memory editing is a feature of the design, not a workaround.

Unset `MEMORY_SHEET_ID` and DIA behaves exactly as before — memory is an
enhancement, never a dependency; an unreachable Sheet degrades to a logged
warning and never blocks or delays an intake.

## Run in Codespaces

This repo includes a `.devcontainer/devcontainer.json` for GitHub Codespaces
Expand Down
21 changes: 21 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,13 +116,34 @@ def handle_process(self) -> None:

self.send_json(response)

# Record the completed run in client memory AFTER the response is on
# the wire, so a slow or unreachable Sheet can never delay an intake.
# No-op unless MEMORY_SHEET_ID is set; never raises.
self._record_engagement(response)

# OBS capture is a LOCAL-ONLY convenience for recording demo videos.
# It is off by default (so the deployed Cloud Run service never pokes a
# non-existent OBS) and only runs when OBS_CAPTURE is explicitly enabled
# locally and the intake surfaced issues. It never affects the response.
if response.get("issues") and _obs_capture_enabled():
self._trigger_obs_capture()

def _record_engagement(self, response: dict[str, object]) -> None:
try:
from client_discovery import memory
from client_discovery.models import ClientIntake, OpportunityScore

if not memory.is_enabled():

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Load .env before gating /api/process memory writes

For the documented local flow (python app.py with MEMORY_SHEET_ID in .env), posting directly to /api/process reaches this check before any module has loaded dotenv; app.py imports client_discovery.config but never calls its loader, and only the agent path loads .env. As a result the deterministic path silently skips recording every intake until /api/agent or /api/agent/status happens to import agent_runtime, so MEMORY_SHEET_ID should be loaded before this is_enabled() gate (or inside the memory config helper).

Useful? React with 👍 / 👎.

return
memory.record_engagement(
ClientIntake(**response["intake"]),
OpportunityScore(**response["score"]),
source="/api/process",
documents=sorted(response.get("documents", {})),
)
except Exception as error: # noqa: BLE001 - memory must never break intake
logger.warning("client memory recording skipped: %s", error)

def _trigger_obs_capture(self) -> None:
try:
from client_discovery.core import (
Expand Down
26 changes: 25 additions & 1 deletion character.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
score_opportunity,
validate_intake,
)
from client_discovery import memory

REPO_ROOT = Path(__file__).resolve().parent

Expand Down Expand Up @@ -45,7 +46,20 @@ def generate_intake_documents(questionnaire_markdown: str) -> dict:
"""Generate profile, opportunity analysis, and proposal draft markdown."""
intake = parse_questionnaire_markdown(questionnaire_markdown)
score = score_opportunity(intake)
return generate_documents(intake, score)
documents = generate_documents(intake, score)
# Record the completed run in client memory (no-op unless MEMORY_SHEET_ID
# is set; never raises). This is the agent path's write trigger — the
# deterministic /api/process path records in app.py.
memory.record_engagement(
intake, score, source="/api/agent", documents=sorted(documents)
)
return documents


def recall_client_history(company_name: str, website: str = "") -> dict:
"""Look up whether this company has been seen before and return its
prior engagement history (client record + recent intake runs)."""
return memory.recall_summary(company_name, website)


def _make_mcp_toolset() -> list:
Expand Down Expand Up @@ -142,6 +156,12 @@ def build_agent(
score_client_opportunity,
generate_intake_documents,
]
# Plain-function tool, not MCP (spec Open Question #6): auto-included when
# MEMORY_SHEET_ID is set, same present-means-on pattern as the MCP URLs.
# Cheap to include unconditionally per turn — it's a local function, not a
# network toolset, so it needs no per-message keyword routing.
if memory.is_enabled():
tools.append(recall_client_history)
if include_make_mcp:
tools.extend(_make_mcp_toolset())
if include_gcp_mcp:
Expand Down Expand Up @@ -172,6 +192,10 @@ def build_agent(
- When GCP Cloud tools are available (monitoring, logging, etc.), use them
for operational questions about the agent itself, e.g. its own request
metrics or recent errors. Report exactly which tool ran and what it returned.
- When the recall_client_history tool is available and an intake names a
company, check for prior history before drafting recommendations. If the
client is known, reference the prior engagement context (last tier, prior
pain points, proposal status) instead of treating them as brand new.
""",
generate_content_config=types.GenerateContentConfig(
http_options=types.HttpOptions(
Expand Down
Loading
Loading