Skip to content

FULL END TO END FLOW — ERP Agent with RAG + Excel Export

THE FULL END TO END FLOW — ERP Agent with RAG + Excel Export


The System Setup — Configured Once, Sent Every Call

System Prompt:

You are a business intelligence assistant for an ERP system.

## Behaviour
- When a user asks a vague question, do not ask for clarification.
  Make reasonable assumptions, state them explicitly, then answer.
- Always fetch data using tools before answering. Never guess numbers.
- When answering always provide:
    - Direct answer to the question
    - Supporting breakdown
    - Assumptions you made
    - A follow-up suggestion where relevant

## Export Rules
- User asks for Excel / spreadsheet / .xlsx 
  call export_excel. Pass all data as a clean CSV string.
  Include a header row. Pure CSV only  no markdown, no formatting.

- User asks for PDF / printable report 
  call export_pdf. Pass data as clean HTML with <table> tags.
  Inline styles only. Include an <h1> title.

- "download this", "save this", "send me a file" with no format stated 
  ask which format before calling any export tool.

- Never embed raw file data in your text response.
  Always use the export tools. Tell the user the file is ready with the URL.

## RAG Tool Usage
- When the user asks about historical interactions, complaints, past issues,
  support history, or anything that is not structured data 
  call search_customer_history before answering.
- Never guess historical context. Always retrieve it.

## Today's date: 2026-05-09

Tool Definitions — sent with every API call:

[
  {
    "name": "get_inactive_customers",
    "description": "Gets customers with no ERP activity in the past N months. Returns customer ID, name, segment, last activity date, account manager.",
    "input_schema": {
      "type": "object",
      "properties": {
        "months": {
          "type": "integer",
          "description": "How many months back to check for inactivity"
        }
      },
      "required": ["months"]
    }
  },
  {
    "name": "get_customer_activity_summary",
    "description": "Gets activity summary for a batch of customers. Returns order count, last order value, lifetime value, payment status. Always call this after get_inactive_customers to enrich the data.",
    "input_schema": {
      "type": "object",
      "properties": {
        "customer_ids": {
          "type": "array",
          "items": { "type": "integer" },
          "description": "List of customer IDs — pass all at once, not one at a time"
        }
      },
      "required": ["customer_ids"]
    }
  },
  {
    "name": "get_customer_order_frequency",
    "description": "Gets order frequency tier for a batch of customers over a given period. Returns: frequent / infrequent / never-frequent per customer.",
    "input_schema": {
      "type": "object",
      "properties": {
        "customer_ids": {
          "type": "array",
          "items": { "type": "integer" }
        },
        "months": {
          "type": "integer",
          "description": "Period in months to analyse frequency over"
        }
      },
      "required": ["customer_ids", "months"]
    }
  },
  {
    "name": "search_customer_history",
    "description": "Semantic search across historical support tickets, customer emails, and communication logs. Use when the user asks about past issues, complaints, historical interactions, or anything not in structured tables.",
    "input_schema": {
      "type": "object",
      "properties": {
        "query": {
          "type": "string",
          "description": "Natural language search query describing what to look for"
        },
        "customer_name": {
          "type": "string",
          "description": "Optional — scope the search to a specific customer"
        }
      },
      "required": ["query"]
    }
  },
  {
    "name": "export_excel",
    "description": "Generates an Excel file from CSV data. Call when user requests Excel, spreadsheet, or .xlsx. Returns a download URL.",
    "input_schema": {
      "type": "object",
      "properties": {
        "filename": {
          "type": "string",
          "description": "Filename without extension"
        },
        "csv": {
          "type": "string",
          "description": "Full CSV string including header row. Clean data only — no markdown, no extra formatting."
        }
      },
      "required": ["filename", "csv"]
    }
  }
]

TURN 1 — "show me all customers who have not had any activity in past x months"


→ API Call 1: Your App → Anthropic

{
  "model": "claude-sonnet-4-6",
  "max_tokens": 4096,
  "system": "...system prompt above...",
  "tools": [...all five tool definitions...],
  "messages": [
    {
      "role": "user",
      "content": "show me all customers who have not had any activity in past x months"
    }
  ]
}

One message. Full tool list. System prompt. Nothing else.


← API Response 1: Anthropic → Your App

{
  "id": "msg_001",
  "role": "assistant",
  "stop_reason": "tool_use",
  "content": [
    {
      "type": "tool_use",
      "id": "call_001",
      "name": "get_inactive_customers",
      "input": {
        "months": 6
      }
    }
  ]
}

What happened:

  • User said "x months" — no number
  • Claude assumed 6. Reasonable industry default
  • No text block. Claude went straight to the tool call
  • stop_reason: tool_use — Claude has stopped. It is waiting. It called nothing
  • Your loop reads call_001, reads get_inactive_customers, reads months: 6

Your app executes:

var result = await GetInactiveCustomers(months: 6);
// runs: SELECT customer_id, name, segment, last_activity_date, account_manager
//       FROM customers
//       WHERE last_activity_date < NOW() - INTERVAL '6 months'

DB returns:

[
  { "customer_id": 101, "name": "Acme Corp",     "segment": "Enterprise",     "last_activity": "2025-08-12", "account_manager": "Sarah Jones" },
  { "customer_id": 102, "name": "BuildCo Ltd",   "segment": "Enterprise",     "last_activity": "2025-07-03", "account_manager": "Mike Peters" },
  { "customer_id": 103, "name": "TechStart Inc", "segment": "Mid-Market",     "last_activity": "2025-09-01", "account_manager": "Sarah Jones" },
  { "customer_id": 104, "name": "RetailPlus",    "segment": "Mid-Market",     "last_activity": "2025-08-22", "account_manager": "Dan Walsh"   },
  { "customer_id": 105, "name": "QuickTrade",    "segment": "Small Business", "last_activity": "2025-06-15", "account_manager": "Mike Peters" }
]

50 customers total. Truncated here for readability.


→ API Call 2: Your App → Anthropic

You append the assistant message and tool result to history. Full payload:

{
  "model": "claude-sonnet-4-6",
  "max_tokens": 4096,
  "system": "...system prompt...",
  "tools": [...all tools...],
  "messages": [
    {
      "role": "user",
      "content": "show me all customers who have not had any activity in past x months"
    },
    {
      "role": "assistant",
      "content": [
        {
          "type": "tool_use",
          "id": "call_001",
          "name": "get_inactive_customers",
          "input": { "months": 6 }
        }
      ]
    },
    {
      "role": "user",
      "content": [
        {
          "type": "tool_result",
          "tool_use_id": "call_001",
          "content": "[{\"customer_id\":101,\"name\":\"Acme Corp\",\"segment\":\"Enterprise\",\"last_activity\":\"2025-08-12\",\"account_manager\":\"Sarah Jones\"},{\"customer_id\":102,...}]"
        }
      ]
    }
  ]
}

Critical structure notes:

  • The assistant message echoes back the exact tool_use block Claude returned — verbatim
  • The tool result is a user role message — not assistant
  • tool_use_id: "call_001" matches — this is how Claude pairs the result to the call
  • The content is your serialised DB result as a string

← API Response 2: Anthropic → Your App

Claude has 50 customers. Only IDs, names, segments, dates. It wants richer data.

{
  "id": "msg_002",
  "role": "assistant",
  "stop_reason": "tool_use",
  "content": [
    {
      "type": "tool_use",
      "id": "call_002",
      "name": "get_customer_activity_summary",
      "input": {
        "customer_ids": [101, 102, 103, 104, 105, 106, 107, 108, 109, 110,
                         111, 112, 113, 114, 115, 116, 117, 118, 119, 120,
                         121, 122, 123, 124, 125, 126, 127, 128, 129, 130,
                         131, 132, 133, 134, 135, 136, 137, 138, 139, 140,
                         141, 142, 143, 144, 145, 146, 147, 148, 149, 150]
      }
    }
  ]
}

What happened:

  • Claude batched all 50 IDs into one call — not 50 individual calls
  • This is because you defined the tool with an array parameter
  • Tool description said "pass all at once, not one at a time" — Claude followed it

Your app executes:

var result = await GetCustomerActivitySummary(customerIds: [...50 ids...]);
// runs: SELECT c.customer_id, c.name,
//              COUNT(o.id) as total_orders,
//              MAX(o.value) as last_order_value,
//              SUM(o.value) as lifetime_value,
//              ps.status as payment_status
//       FROM customers c
//       LEFT JOIN orders o ON o.customer_id = c.id
//       LEFT JOIN payment_status ps ON ps.customer_id = c.id
//       WHERE c.customer_id = ANY(@ids)
//       GROUP BY c.customer_id, ps.status

DB returns:

[
  { "customer_id": 101, "name": "Acme Corp",   "total_orders": 45, "last_order_value": 1200.00, "lifetime_value": 87400.00, "payment_status": "Good" },
  { "customer_id": 102, "name": "BuildCo Ltd", "total_orders": 38, "last_order_value": 3400.00, "lifetime_value": 62000.00, "payment_status": "Good" },
  { "customer_id": 103, "name": "TechStart",   "total_orders": 12, "last_order_value": 450.00,  "lifetime_value": 8200.00,  "payment_status": "Late" },
  { "customer_id": 104, "name": "RetailPlus",  "total_orders": 6,  "last_order_value": 220.00,  "lifetime_value": 3100.00,  "payment_status": "Good" },
  { "customer_id": 105, "name": "QuickTrade",  "total_orders": 4,  "last_order_value": 180.00,  "lifetime_value": 1800.00,  "payment_status": "Good" }
]

→ API Call 3: Your App → Anthropic

{
  "messages": [
    { "role": "user",      "content": "show me all customers..." },
    { "role": "assistant", "content": [{ "type": "tool_use", "id": "call_001", "name": "get_inactive_customers", "input": {"months":6} }] },
    { "role": "user",      "content": [{ "type": "tool_result", "tool_use_id": "call_001", "content": "[...50 customers...]" }] },
    { "role": "assistant", "content": [{ "type": "tool_use", "id": "call_002", "name": "get_customer_activity_summary", "input": {"customer_ids":[101,102,...50]} }] },
    { "role": "user",      "content": [{ "type": "tool_result", "tool_use_id": "call_002", "content": "[...50 summaries...]" }] }
  ]
}

History is growing. Every prior exchange replayed in full. Claude sees: original question, both tool calls, both results.


← API Response 3: Anthropic → Your App — end_turn

{
  "id": "msg_003",
  "role": "assistant",
  "stop_reason": "end_turn",
  "content": [
    {
      "type": "text",
      "text": "## Inactive Customers — Last 6 Months\n\n**Assumption:** Interpreted \"past x months\" as 6 months since no timeframe was specified.\n\n50 customers have had no recorded activity since November 2025.\n\n### By Segment\n| Segment | Count | % of Inactive |\n|---------|-------|---------------|\n| Enterprise | 12 | 24% |\n| Mid-Market | 18 | 36% |\n| Small Business | 20 | 40% |\n\n### Notable High-Value Accounts\n| Customer | Segment | Last Activity | Lifetime Value | Account Manager |\n|----------|---------|---------------|----------------|-----------------|\n| Acme Corp | Enterprise | Aug 2025 | £87,400 | Sarah Jones |\n| BuildCo Ltd | Enterprise | Jul 2025 | £62,000 | Mike Peters |\n| TechStart Inc | Mid-Market | Sep 2025 | £8,200 | Sarah Jones |\n\n**Suggested next step:** Would you like me to identify which of these were previously frequent buyers to help prioritise re-engagement outreach?"
    }
  ]
}

What happened:

  • stop_reason: end_turn — done for this turn
  • Single text block — markdown formatted
  • Claude stated its assumption about "x months"
  • Suggested a follow-up unprompted because system prompt told it to
  • Your app appends to session history, saves to Redis/DB, returns text to frontend
  • Frontend renders markdown — tables appear properly

TURN 2 — "and how many of these users have not been frequent to the platform"


→ API Call 4: Your App → Anthropic

Append new user message to history. Send everything:

{
  "messages": [
    { "role": "user",      "content": "show me all customers who have not had any activity in past x months" },
    { "role": "assistant", "content": [{ "type": "tool_use", "id": "call_001", ... }] },
    { "role": "user",      "content": [{ "type": "tool_result", "tool_use_id": "call_001", "content": "[...50 customers...]" }] },
    { "role": "assistant", "content": [{ "type": "tool_use", "id": "call_002", ... }] },
    { "role": "user",      "content": [{ "type": "tool_result", "tool_use_id": "call_002", "content": "[...50 summaries...]" }] },
    { "role": "assistant", "content": [{ "type": "text", "text": "## Inactive Customers — Last 6 Months..." }] },
    {
      "role": "user",
      "content": "and how many of these users have not been frequent to the platform"
    }
  ]
}

Critical: Claude has the 50 customer IDs in context. It will not re-call get_inactive_customers. It already knows who they are.


← API Response 4: Anthropic → Your App

{
  "id": "msg_004",
  "role": "assistant",
  "stop_reason": "tool_use",
  "content": [
    {
      "type": "tool_use",
      "id": "call_003",
      "name": "get_customer_order_frequency",
      "input": {
        "customer_ids": [101, 102, 103, 104, 105, ...all 50],
        "months": 12
      }
    }
  ]
}

What happened:

  • Claude pulled IDs from earlier context — zero re-fetching
  • "frequent" is undefined — Claude will define it, state the definition
  • Picked 12 months as a reasonable measurement period — will state this
  • Batched all 50

Your app executes and DB returns:

[
  { "customer_id": 101, "name": "Acme Corp",   "orders_per_month": 3.8, "frequency_tier": "frequent"       },
  { "customer_id": 102, "name": "BuildCo Ltd", "orders_per_month": 2.9, "frequency_tier": "frequent"       },
  { "customer_id": 103, "name": "TechStart",   "orders_per_month": 0.4, "frequency_tier": "infrequent"     },
  { "customer_id": 104, "name": "RetailPlus",  "orders_per_month": 0.1, "frequency_tier": "never-frequent" },
  { "customer_id": 105, "name": "QuickTrade",  "orders_per_month": 0.2, "frequency_tier": "never-frequent" }
]

→ API Call 5 → ← API Response 5: end_turn

Full history sent. Claude responds:

{
  "id": "msg_005",
  "stop_reason": "end_turn",
  "content": [
    {
      "type": "text",
      "text": "## Frequency Analysis of 50 Inactive Customers\n\n**Assumptions:**\n- Defined \"frequent\" as 2+ orders per month averaged over the past 12 months\n- Period: May 2025 – May 2026\n\n### Breakdown\n| Frequency Tier | Count | % of Inactive |\n|----------------|-------|---------------|\n| Previously frequent (now gone quiet) | 16 | 32% |\n| Infrequent (1–2 orders/month) | 22 | 44% |\n| Never frequent (<1 order/month) | 12 | 24% |\n\n**34 of 50 were already infrequent or never frequent.** Different strategy needed for these versus the 16 who dropped off.\n\n### The 16 Previously Frequent — Highest Re-engagement Priority\n| Customer | Avg Orders/Month | Last Activity | Lifetime Value | Account Manager |\n|----------|-----------------|---------------|----------------|-----------------|\n| Acme Corp | 3.8 | Aug 2025 | £87,400 | Sarah Jones |\n| BuildCo Ltd | 2.9 | Jul 2025 | £62,000 | Mike Peters |\n| ... | | | | |\n\nWould you like me to export this full list to Excel?"
    }
  ]
}

Claude suggested the export. It saw the export tool, the data, and made the connection.


TURN 3 — "yes export to excel"


→ API Call 6: Your App → Anthropic

{
  "messages": [
    ...all 5 turns of history...,
    {
      "role": "user",
      "content": "yes export to excel"
    }
  ]
}

Claude has in context:

  • 50 customer names, segments, last activity dates, account managers — from call_001 result
  • Lifetime values, order counts, payment status — from call_002 result
  • Frequency tiers, orders per month — from call_003 result

It has everything. No data tools needed.


← API Response 6: Anthropic → Your App

{
  "id": "msg_006",
  "role": "assistant",
  "stop_reason": "tool_use",
  "content": [
    {
      "type": "tool_use",
      "id": "call_004",
      "name": "export_excel",
      "input": {
        "filename": "inactive-customers-may-2026",
        "csv": "Customer,Segment,Last Activity,Lifetime Value (GBP),Avg Orders/Month,Frequency Tier,Account Manager\nAcme Corp,Enterprise,2025-08-12,87400,3.8,Previously Frequent,Sarah Jones\nBuildCo Ltd,Enterprise,2025-07-03,62000,2.9,Previously Frequent,Mike Peters\nTechStart Inc,Mid-Market,2025-09-01,8200,0.4,Infrequent,Sarah Jones\nRetailPlus,Mid-Market,2025-08-22,3100,0.1,Never Frequent,Dan Walsh\nQuickTrade,Small Business,2025-06-15,1800,0.2,Never Frequent,Mike Peters\n..."
      }
    }
  ]
}

What happened — this is the key moment:

  • Claude constructed the entire CSV from data sitting in message history
  • It joined three separate tool results together in memory — inactive list + summary + frequency
  • It picked sensible column headers and ordering
  • It sorted by lifetime value descending without being asked — applied judgement
  • stop_reason: tool_use — waiting for you to generate the file
  • Claude never touched your filesystem. It just produced a CSV string.

Your app executes export_excel:

public async Task<string> ExportExcel(string filename, string csv)
{
    using var workbook = new XLWorkbook();
    var sheet = workbook.AddWorksheet("Inactive Customers");
    var rows  = csv.Split('\n');

    for (int r = 0; r < rows.Length; r++)
    {
        var cols = rows[r].Split(',');
        for (int c = 0; c < cols.Length; c++)
            sheet.Cell(r + 1, c + 1).Value = cols[c].Trim('"');
    }

    // Style header
    var header = sheet.Row(1);
    header.Style.Font.Bold = true;
    header.Style.Fill.BackgroundColor = XLColor.FromHtml("#1e1e2e");
    header.Style.Font.FontColor = XLColor.White;
    sheet.Columns().AdjustToContents();

    using var ms = new MemoryStream();
    workbook.SaveAs(ms);

    // Save to blob storage, get back a URL
    var url = await _fileStore.SaveAsync($"{filename}.xlsx", ms.ToArray());

    return JsonSerializer.Serialize(new {
        url      = url,
        filename = $"{filename}.xlsx"
    });
}

Tool returns:

{ "url": "/files/inactive-customers-may-2026.xlsx", "filename": "inactive-customers-may-2026.xlsx" }

→ API Call 7: Your App → Anthropic

{
  "messages": [
    ...all previous history...,
    {
      "role": "assistant",
      "content": [
        {
          "type": "tool_use",
          "id": "call_004",
          "name": "export_excel",
          "input": {
            "filename": "inactive-customers-may-2026",
            "csv": "Customer,Segment,Last Activity,..."
          }
        }
      ]
    },
    {
      "role": "user",
      "content": [
        {
          "type": "tool_result",
          "tool_use_id": "call_004",
          "content": "{\"url\":\"/files/inactive-customers-may-2026.xlsx\",\"filename\":\"inactive-customers-may-2026.xlsx\"}"
        }
      ]
    }
  ]
}

← API Response 7: Anthropic → Your App — Final

{
  "id": "msg_007",
  "role": "assistant",
  "stop_reason": "end_turn",
  "content": [
    {
      "type": "text",
      "text": "Your Excel file is ready.\n\n**[Download inactive-customers-may-2026.xlsx](/files/inactive-customers-may-2026.xlsx)**\n\nThe file includes all 50 inactive customers with segment, last activity date, lifetime value, order frequency tier, and account manager — sorted by lifetime value descending so your highest-value at-risk accounts are at the top."
    }
  ]
}

Your app saves session. Returns text to frontend. Frontend renders markdown — download link is a real clickable anchor.


TURN 4 — "has Acme Corp ever complained about delivery issues"

This is where RAG enters. This question cannot be answered from structured tables.


→ API Call 8: Your App → Anthropic

{
  "messages": [
    ...all 7 turns of history...,
    {
      "role": "user",
      "content": "has Acme Corp ever complained about delivery issues"
    }
  ]
}

← API Response 8: Anthropic → Your App

{
  "id": "msg_008",
  "role": "assistant",
  "stop_reason": "tool_use",
  "content": [
    {
      "type": "tool_use",
      "id": "call_005",
      "name": "search_customer_history",
      "input": {
        "query": "Acme Corp delivery complaints late shipment issues",
        "customer_name": "Acme Corp"
      }
    }
  ]
}

What happened:

  • Claude recognised this is a historical question — not answerable from structured data
  • It reached for search_customer_history — the RAG tool
  • It formed a natural language query from the user's question
  • It scoped the search to Acme Corp
  • Your loop handles this identically to any other tool call

Your app executes search_customer_history:

public async Task<string> SearchCustomerHistory(string query, string? customerName = null)
{
    // 1. Embed the query using your embedding model
    var queryVector = await _embedder.EmbedAsync(query);
    // queryVector is now float[1536] — a mathematical representation of meaning

    // 2. Vector search in pgvector
    // SQL: SELECT text, source, created_at,
    //             1 - (embedding <=> $1) AS similarity
    //      FROM document_chunks
    //      WHERE ($2 IS NULL OR customer_name = $2)
    //      ORDER BY embedding <=> $1
    //      LIMIT 5
    var results = await _vectorDb.SearchAsync(
        vector:       queryVector,
        customerName: customerName,
        topK:         5);

    // 3. Format results as readable text for Claude
    return string.Join("\n\n---\n\n", results.Select(r =>
        $"[{r.Source} — {r.Date:yyyy-MM-dd}]\n{r.Text}"));
}

pgvector finds and returns the 5 most semantically similar chunks:

[Support Ticket #4421 — 2024-03-12]
Acme Corp reported that order #8821 arrived 4 days past the committed 
delivery date. Customer contact Sarah Mitchell expressed frustration and 
asked for a credit note. Resolved with 5% discount on next order.

---

[Support Ticket #3892 — 2023-11-08]
Follow-up call from Acme Corp regarding delayed shipment on order #7103. 
Original ETA was 2023-10-28. Actual delivery 2023-11-06. No escalation, 
customer accepted apology.

---

[Email Thread — 2024-01-15]
Sarah from Acme Corp wrote asking about status of order #8104 which was 
showing as dispatched but not received after 8 days. Tracked and confirmed 
carrier delay. Customer satisfied with response.

---

[Support Ticket #2201 — 2023-06-22]
Acme Corp raised issue about damaged packaging on delivery of order #6540.
Not strictly a late delivery but flagged under logistics complaints. 
Replacement sent same day.

---

[Internal Note — 2024-02-03]
Account manager Sarah Jones flagged Acme Corp as sensitive on delivery 
commitments following repeated incidents in 2023-2024. Recommend 
prioritising their orders in dispatch queue.

What just happened under the hood in pgvector:

  • Your query "Acme Corp delivery complaints late shipment issues" was converted to a vector
  • pgvector compared that vector against every stored chunk using cosine distance
  • The 5 chunks whose vectors were geometrically closest came back
  • None of them necessarily contained the exact words "delivery complaints" — they matched on meaning

→ API Call 9: Your App → Anthropic

{
  "messages": [
    ...all previous history...,
    {
      "role": "assistant",
      "content": [{ "type": "tool_use", "id": "call_005", "name": "search_customer_history", "input": {...} }]
    },
    {
      "role": "user",
      "content": [
        {
          "type": "tool_result",
          "tool_use_id": "call_005",
          "content": "[Support Ticket #4421 — 2024-03-12]\nAcme Corp reported that order #8821 arrived 4 days past...\n\n---\n\n[Support Ticket #3892 — 2023-11-08]\n..."
        }
      ]
    }
  ]
}

← API Response 9: Anthropic → Your App — end_turn

{
  "id": "msg_009",
  "role": "assistant",
  "stop_reason": "end_turn",
  "content": [
    {
      "type": "text",
      "text": "## Acme Corp — Delivery Complaint History\n\nYes — Acme Corp has raised delivery-related issues on **at least 4 occasions** between June 2023 and March 2024.\n\n| Date | Ticket | Issue | Resolution |\n|------|--------|-------|------------|\n| Mar 2024 | #4421 | Order #8821 arrived 4 days late | 5% discount applied |\n| Jan 2024 | Email | Order #8104 not received after 8 days | Carrier delay confirmed |\n| Nov 2023 | #3892 | Order #7103 delivered 9 days late | Apology accepted |\n| Jun 2023 | #2201 | Damaged packaging on order #6540 | Replacement sent |\n\n**Account manager note (Feb 2024):** Sarah Jones flagged Acme Corp as sensitive on delivery commitments and recommended prioritising their orders in the dispatch queue.\n\nGiven that Acme Corp is also on your inactive list with £87,400 lifetime value, delivery reliability is likely a contributing factor to their disengagement. This is worth addressing directly in any re-engagement outreach."
    }
  ]
}

What happened:

  • Claude synthesised 5 raw ticket chunks into a structured, readable answer
  • It cross-referenced with earlier context — knew Acme Corp's lifetime value from Turn 1
  • It connected the delivery history to the inactivity without being asked
  • That connection is the intelligence — you never hardcoded it

The Complete Message History After All 4 Turns

[01] user       "show me all customers who have not had any activity..."
[02] assistant  tool_use: get_inactive_customers(months: 6)
[03] user       tool_result: call_001  [50 customers]
[04] assistant  tool_use: get_customer_activity_summary(customer_ids: [...50])
[05] user       tool_result: call_002  [50 summaries with lifetime values]
[06] assistant  text: "## Inactive Customers — Last 6 Months..."
[07] user       "and how many of these users have not been frequent..."
[08] assistant  tool_use: get_customer_order_frequency(customer_ids: [...50], months: 12)
[09] user       tool_result: call_003  [frequency tiers for 50]
[10] assistant  text: "## Frequency Analysis of 50 Inactive Customers..."
[11] user       "yes export to excel"
[12] assistant  tool_use: export_excel(filename, csv)
[13] user       tool_result: call_004  { url, filename }
[14] assistant  text: "Your Excel file is ready. Download here..."
[15] user       "has Acme Corp ever complained about delivery issues"
[16] assistant  tool_use: search_customer_history(query, customer_name)
[17] user       tool_result: call_005  [5 relevant ticket chunks]
[18] assistant  text: "## Acme Corp — Delivery Complaint History..."

18 messages. 9 API calls. 4 user turns. 5 autonomous tool decisions by Claude.


Every Decision — Who Made It and Why

Decision Who Why
Assume 6 months for "x" Claude System prompt: assume and state
Fetch activity summary after getting IDs Claude Needed richer data for useful report
Batch all 50 IDs in one call Claude Tool schema had array param + description said so
Define "frequent" as 2+ orders/month Claude Undefined by user, applied judgement
Use 12 months for frequency period Claude Reasonable default, stated it
Suggest export at end of Turn 2 Claude Saw export tool, connected it to context
Construct full CSV from context history Claude All data was already in message history — no re-fetching
Sort CSV by lifetime value descending Claude Applied business judgement unprompted
Use RAG tool for delivery complaint question Claude System prompt: unstructured history = search tool
Form the semantic search query Claude Translated user's question into search terms
Cross-reference delivery history with inactivity Claude Both were in context, made the connection
You ran all SQL queries Your app Claude never touches your DB
You ran the vector search Your app Claude passed a query string, you did the search
You generated the xlsx Your app Claude passed a CSV string, you built the file
You stored the file Your app Claude received the URL back
You saved session history Your app Claude has no memory between calls