> ## Documentation Index
> Fetch the complete documentation index at: https://www.forager.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Forager is a REST API for B2B people, company and job post data: contact lookups (phone numbers, personal emails, work emails), person, company and job post search, and bulk data feeds.
> Server: https://api-v2.forager.ai. Send the API key in the X-API-KEY header. Most paths include {account_id}. Read FORAGER_API_KEY and FORAGER_ACCOUNT_ID from the environment; never put the key in client-side code, logs or prompts.
> To add Forager to a project, follow https://www.forager.ai/docs/guides/agent-quickstart.md. To pick an endpoint, read https://www.forager.ai/docs/guides/choose-an-endpoint.md. For end-to-end builds, see https://www.forager.ai/docs/recipes/overview.md.
> Most requests spend credits. Do not guess costs; use https://www.forager.ai/docs/api-overview/credit-pricing.md and ask the user before spending credits.
> Use only endpoints and fields defined in https://www.forager.ai/docs/openapi.yaml. Forager has no MCP server for the API yet; the /mcp endpoint on this site searches the documentation only.

# Person lookup as an LLM agent tool

> Sample app: an LLM agent tool for person lookup with a framework-agnostic function-calling schema, a server-side executor, a cache and a credit budget.

```text Setup prompt theme={"theme":{"light":"github-light","dark":"github-dark"}}
Implement https://www.forager.ai/docs/samples/agent-person-lookup-tool.md in my project: register a lookup_person tool with our model provider's function-calling API using the JSON Schema on that page, run the tool server-side against the Forager person lookup endpoint, cache results, cap credits per conversation, and return trimmed JSON to the model. The API key stays in FORAGER_API_KEY with FORAGER_ACCOUNT_ID and never reaches the model. Add tests with a fake model and stubbed HTTP.
```

This sample adds one tool to an LLM agent: `lookup_person`, backed by the Forager [person lookup](https://www.forager.ai/docs/openapi/people/datastorage_person_detail_lookup_create.md). The model decides when to call it, your server runs the call, and the model gets back a short JSON summary instead of the full record. The schema is plain JSON Schema, so it fits any provider's function-calling or tool-use API; only the wrapper object around it differs. It is written for the REST API with your API key; Forager does not provide a hosted tool server.

![An LLM agent sends a lookup\_person tool call to your server, which calls the Forager REST API and returns a trimmed result](https://mintcdn.com/forager/OnhSHNo_LBQ7yKjR/images/recipes/ai-agent-tool-calling-b2b-data.svg?fit=max\&auto=format\&n=OnhSHNo_LBQ7yKjR\&q=85\&s=18daa567361830e1e8bbd2aa425573d4)

## How it fits together

1. Your server sends the conversation plus the tool definition to the model.
2. The model answers with a tool call: `lookup_person` and its arguments.
3. Your executor validates the arguments, checks the cache and the credit budget, calls Forager, and trims the record.
4. The trimmed result goes back to the model as the tool result; the model writes the answer.

The key never leaves your server. The model only ever sees the tool definition and the trimmed results.

**1. Define the tool**

One tool, two ways to identify a person. The description tells the model what the call costs so it does not look people up on a whim. Rename the wrapper keys (`parameters` is `input_schema` in some APIs) to match your provider.

```json lookup_person tool definition theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "name": "lookup_person",
  "description": "Look up one professional's public profile (name, headline, current role and employer, location) by their LinkedIn public identifier, the slug after linkedin.com/in/, or by a Forager person_id from an earlier result. Costs 1 credit per call, so call it only when the user asks about a specific person. Returns found=false when there is no match.",
  "parameters": {
    "type": "object",
    "properties": {
      "linkedin_public_identifier": {
        "type": "string",
        "description": "The slug after linkedin.com/in/, for example jane-doe-example. Do not send the full URL."
      },
      "person_id": {
        "type": "integer",
        "description": "A Forager person id returned by a previous lookup_person call."
      }
    },
    "additionalProperties": false
  }
}
```

The request body of the endpoint has the same two optional fields, so the arguments can be forwarded as they are once you have checked that exactly one of them is present.

**2. Write the executor**

The executor is the only code that talks to Forager. It keeps a per-conversation budget (1 credit per call here), caches by identifier so a repeated question is free, treats `403` as fatal, and returns `429` and not-found as data the model can act on.

```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import os
import time
import requests

BASE = f"https://api-v2.forager.ai/api/{os.environ['FORAGER_ACCOUNT_ID']}/datastorage"
HEADERS = {"X-API-KEY": os.environ["FORAGER_API_KEY"]}
CREDITS_PER_LOOKUP = 1
MAX_CREDITS_PER_CONVERSATION = 20


class ForagerAuthError(RuntimeError):
    """403: fix FORAGER_API_KEY / FORAGER_ACCOUNT_ID. Never shown to the model."""


def trim(person):
    current = next((r for r in person.get("roles", []) if r.get("is_current")), None)
    org = (current or {}).get("organization") or {}
    return {
        "found": True,
        "person_id": person["id"],
        "full_name": person.get("full_name"),
        "headline": person.get("headline"),
        "current_title": (current or {}).get("role_title"),
        "current_company": org.get("name") or (current or {}).get("organization_name"),
        "company_domain": org.get("domain"),
        "location": (person.get("location") or {}).get("name"),
        "linkedin_url": (person.get("linkedin_info") or {}).get("public_profile_url"),
    }


class PersonLookupTool:
    def __init__(self):
        self.cache = {}
        self.credits_spent = 0

    def __call__(self, args):
        keys = {k for k in ("linkedin_public_identifier", "person_id") if args.get(k)}
        if len(keys) != 1:
            return {"error": "invalid_arguments", "detail": "send exactly one of linkedin_public_identifier or person_id"}
        key = next(iter(keys)), args[next(iter(keys))]
        if key in self.cache:
            return self.cache[key]
        if self.credits_spent + CREDITS_PER_LOOKUP > MAX_CREDITS_PER_CONVERSATION:
            return {"error": "budget_exhausted", "detail": "no more lookups allowed in this conversation"}
        for attempt in range(2):
            resp = requests.post(f"{BASE}/person_detail_lookup/", headers=HEADERS,
                                 json={key[0]: key[1]}, timeout=30)
            if resp.status_code != 429 or attempt == 1:
                break
            time.sleep(float(resp.headers.get("Retry-After", 2)))
        if resp.status_code == 403:
            raise ForagerAuthError(resp.text)
        if resp.status_code == 429:
            return {"error": "rate_limited", "detail": "try again in a few seconds"}
        if resp.status_code != 200:
            return {"found": False, "detail": f"Forager returned HTTP {resp.status_code}"}
        self.credits_spent += CREDITS_PER_LOOKUP
        result = trim(resp.json())
        self.cache[key] = result
        self.cache[("person_id", result["person_id"])] = result
        return result
```

```javascript JavaScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const BASE = `https://api-v2.forager.ai/api/${process.env.FORAGER_ACCOUNT_ID}/datastorage`;
const HEADERS = { "X-API-KEY": process.env.FORAGER_API_KEY, "Content-Type": "application/json" };
const CREDITS_PER_LOOKUP = 1;
const MAX_CREDITS_PER_CONVERSATION = 20;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

export class ForagerAuthError extends Error {}

function trim(person) {
  const current = (person.roles ?? []).find((r) => r.is_current) ?? null;
  const org = current?.organization ?? {};
  return {
    found: true,
    person_id: person.id,
    full_name: person.full_name,
    headline: person.headline,
    current_title: current?.role_title ?? null,
    current_company: org.name ?? current?.organization_name ?? null,
    company_domain: org.domain ?? null,
    location: person.location?.name ?? null,
    linkedin_url: person.linkedin_info?.public_profile_url ?? null,
  };
}

export class PersonLookupTool {
  cache = new Map();
  creditsSpent = 0;

  async call(args) {
    const keys = ["linkedin_public_identifier", "person_id"].filter((k) => args?.[k]);
    if (keys.length !== 1) {
      return { error: "invalid_arguments", detail: "send exactly one of linkedin_public_identifier or person_id" };
    }
    const [field] = keys;
    const cacheKey = `${field}:${args[field]}`;
    if (this.cache.has(cacheKey)) return this.cache.get(cacheKey);
    if (this.creditsSpent + CREDITS_PER_LOOKUP > MAX_CREDITS_PER_CONVERSATION) {
      return { error: "budget_exhausted", detail: "no more lookups allowed in this conversation" };
    }
    let resp;
    for (let attempt = 0; attempt < 2; attempt++) {
      resp = await fetch(`${BASE}/person_detail_lookup/`, {
        method: "POST", headers: HEADERS, body: JSON.stringify({ [field]: args[field] }),
      });
      if (resp.status !== 429 || attempt === 1) break;
      const retryAfter = Number(resp.headers.get("retry-after"));
      await sleep(retryAfter > 0 ? retryAfter * 1000 : 2000);
    }
    if (resp.status === 403) throw new ForagerAuthError(await resp.text());
    if (resp.status === 429) return { error: "rate_limited", detail: "try again in a few seconds" };
    if (resp.status !== 200) return { found: false, detail: `Forager returned HTTP ${resp.status}` };
    this.creditsSpent += CREDITS_PER_LOOKUP;
    const result = trim(await resp.json());
    this.cache.set(cacheKey, result);
    this.cache.set(`person_id:${result.person_id}`, result);
    return result;
  }
}
```

What the model sees for a hit, with fictional values:

```json Tool result (illustrative) theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "found": true,
  "person_id": 48213,
  "full_name": "Jane Doe",
  "headline": "Head of Data at Acme Example",
  "current_title": "Head of Data",
  "current_company": "Acme Example",
  "company_domain": "acme.com",
  "location": "Austin, Texas, United States",
  "linkedin_url": "https://www.linkedin.com/in/jane-doe-example/"
}
```

**3. Run the tool loop**

`call_model` is the one function you write around your provider's API. It takes the messages and the tool list and returns the reply text plus any tool calls, each with an `id`, a `name` and parsed `arguments`. The loop below is the same for every provider; only the two `messages.append` lines use provider-specific shapes.

```python agent.py theme={"theme":{"light":"github-light","dark":"github-dark"}}
import json

TOOLS = [json.load(open("lookup_person.json"))]   # the definition from step 1
MAX_TOOL_CALLS = 8


def call_model(messages, tools):
    # Replace with a call to your model provider's tool-calling API.
    raise NotImplementedError


def run(user_message):
    tool = PersonLookupTool()
    messages = [{"role": "user", "content": user_message}]
    calls = 0
    while True:
        reply = call_model(messages, TOOLS)
        if not reply.tool_calls:
            return reply.text
        messages.append(reply.raw)                       # the assistant turn, in your provider's format
        for call in reply.tool_calls:
            calls += 1
            if call.name != "lookup_person":
                result = {"error": "unknown_tool"}
            elif calls > MAX_TOOL_CALLS:
                result = {"error": "call_budget_reached"}
            else:
                result = tool(call.arguments)
            messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})
```

A `ForagerAuthError` propagates out of `run()`: it is a deployment problem, and the model should never be asked to work around it.

**4. Add a system prompt line**

Tell the model how to treat the tool so it asks for a handle instead of guessing one:

```text System prompt fragment theme={"theme":{"light":"github-light","dark":"github-dark"}}
You can look up a professional's public profile with lookup_person. Call it only when the user names a specific person and you have their LinkedIn public identifier (the part after linkedin.com/in/) or a person_id from an earlier result. Never guess an identifier. Each call costs the user 1 credit; if the result is found=false, say so and ask for a different identifier instead of retrying.
```

## Test it without a model or credits

* Replace `call_model` with a fake that returns one `lookup_person` call, then a final text reply, and assert the loop sends the tool result back.
* Stub the HTTP layer: a `200` with a sample record checks `trim()`, a second identical call checks the cache (no second HTTP request), a `429` then `200` checks the retry, a `403` checks that `ForagerAuthError` escapes.
* Assert `budget_exhausted` after `MAX_CREDITS_PER_CONVERSATION` worth of distinct lookups.

## Extend it

* Add a second tool for contact data only when the product needs it: a [phone number lookup](https://www.forager.ai/docs/openapi/phones/datastorage_person_contacts_lookup_phone_numbers_create.md) costs 15 credits, so give it its own, smaller budget and a description that says so. The [agent tool calling recipe](https://www.forager.ai/docs/recipes/agent-tool-calling.md) shows a two-tool setup with people search and phone numbers.
* For "who is the Head of Data at acme.com" questions, add a [people search](https://www.forager.ai/docs/guides/search-people-by-role.md) tool and have the model pass the `person.id` it returns into `lookup_person`.
* Log every tool call with its arguments and credits, and show the running total to the user.

## Related

> [card] [Agent tool calling recipe](https://www.forager.ai/docs/recipes/agent-tool-calling.md)
>
> Two tools, search then phone lookup, with the same loop.

> [card] [Build with AI agents](https://www.forager.ai/docs/guides/build-with-agents.md)
>
> The OpenAPI spec, llms.txt and the Forager API skill.

> [card] [Person lookup reference](https://www.forager.ai/docs/openapi/people/datastorage_person_detail_lookup_create.md)
>
> Every field in the person record.

> [card] [Credit pricing](https://www.forager.ai/docs/api-overview/credit-pricing.md)
>
> What each call costs.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.