# Job Search API with LangChain

Python 3.12 and `langchain-core==1.6.5` were tested locally. These thin REST tools work without MCP being open. They are an unpublished example, not an official LangChain registry integration. Tests use the real LangChain tool mechanism and a loopback HTTP fixture; no paid model or live TrueFetch collection has been tested.


## Canonical submission and original-command recovery

Use `POST /v1/quotes`, then `POST /v1/tasks` with `productId`, the approved input, the quoted `expectedPricingVersion`, and the canonical `Idempotency-Key`. The host creates a key **once for explicitly approved new work**, using `new_command_key()` from the bundled REST engine, and persists it with the approved input before submission. Keys have the form `v1.<13-digit Unix milliseconds>.<22–64 base64url characters>`. Do not generate keys in model arguments or a workflow retry expression.

Creation requires `allow_write=True`, the persisted `request_key`, and explicit `command_mode="new"`. The default `command_mode="recover"` performs an original-command GET and never obtains another quote or submits again. Repeated invocation in the same new-intent toolset also reads the original after its first submission attempt. Changed input is refused. After a restart or any lost response, use this host-only path; it needs no input, quote or model call:

```python
import os
from truefetch_rest import recover_command, observed_task_id

original = recover_command(
    os.environ["TRUEFETCH_API_KEY"], "tasks.create", os.environ["TRUEFETCH_COMMAND_KEY"]
)
if original["receipt"]["outcome"] == "accepted":
    saved_task_id = observed_task_id(original)
    # Persist the reference before reading the existing task/results.
```

HTTP 200 is not acceptance. Inspect `receipt.outcome`: `accepted` supplies an authoritative task reference even when the current `task` projection is null; `processing` and `unknown` require another explicit original GET, respecting the returned retry interval. `rejected` and `not_accepted_final` establish nonacceptance and require deliberate new work if you still want a collection. `receipt_expired` remains unconfirmed: inspect Tasks or contact support. The helper never seals, replays a POST or creates a replacement key. The server accepts new keys for 24 hours, allows five minutes of future clock skew, and retains terminal receipts for at least seven days; it can still answer a retained original after the new-command window ends. A current Task state does not establish a particular command's outcome.

Cancellation is a separate command: bind a different persisted canonical `cancel_key` for the newly created task, or pass `cancel_task_ids=(saved_task_id,)` and `cancel_command_keys={saved_task_id: saved_cancel_key}` for an explicitly approved saved target. `POST /v1/tasks/{taskId}/cancel` sends `{}` and that cancellation key. Unknown cancellation is recovered with `recover_command(api_key, "tasks.cancel", saved_cancel_key)`, never the creation key. Acceptance of cancellation does not promise the task ended as cancelled. The budget, keys and cancellation mapping remain host-only arguments.


## Install

Create a Python virtual environment and install `requirements.txt`. Keep `truefetch_tools.py`, `truefetch_rest.py` and `agent_example.py` on your Python import path. The shared REST engine is in `examples/python/` in the checkout and is bundled alongside the adapter in the download kit. Store your TrueFetch key in `TRUEFETCH_API_KEY`, outside source files and prompts. The example does not load environment files automatically.

```sh
python -m pip install -r requirements.txt
```

The repository checkout also includes `test_tools.py`; run `python -m unittest discover -s . -p test_tools.py -v` there for regressions. Test fixtures are not included in the website download kit.

## One collection, complete results

The application owns the budget and stable request key. The model cannot supply a new key or raise the budget through tool arguments. The key identifies one intended collection, not one execution attempt. Persist it with the original input before submitting; retain it if any response is lost.

```python
import os
from truefetch_tools import make_tools, recover_results

tools = make_tools(
    os.environ["TRUEFETCH_API_KEY"],
    max_credits=100,
    request_key=os.environ["TRUEFETCH_COMMAND_KEY"],
    command_mode="new",
    allow_write=True,
)
by_name = {t.name: t for t in tools}
job_input = {
    "keyword": "Software engineer",
    "country": "Germany",
    "platforms": ["arbeitnow"],
    "maxResults": 10,
}

# Read-only estimate. Creation also refreshes and checks the estimate.
estimate = by_name["truefetch_estimate"].invoke({"input": job_input})
accepted = by_name["truefetch_search_jobs"].invoke({"input": job_input})
if accepted["receipt"]["outcome"] != "accepted":
    raise RuntimeError("Command is not accepted; inspect its original receipt before new work")
task_id = next(
    ref["id"] for ref in accepted["receipt"]["result"]["objects"] if ref["kind"] == "task"
)
print("Save this task ID before further processing:", task_id)

# Polls the same task, reads every page, verifies count/order, and preserves partial status.
result = recover_results(tools, task_id, timeout_seconds=300)
for job in result["data"]:
    print(job["recordId"], job.get("platformUrl"))
```

Creation reserves credits and may incur charges. `max_credits` is a per-collection budget, not a total monthly spending cap; configure API-key spending limits separately. One credit equals 1,000,000 billing units. The node sends the quoted pricing version and budget to the server. A pricing conflict stops creation. The example does not automatically retry HTTP requests.

To recover after restart, instantiate read-only tools with the same account's key and call `recover_results(tools, saved_task_id)`. Do not call creation again if the task ID is known. If acceptance is unknown, read the original receipt with its saved key, without requoting or submitting. Original-command recovery does not need a quote; use the saved key before deciding on new work. A local timeout does not cancel the remote task.

## Bind to a model

Recovery checks its deadline before and after tool reads, during pagination and before returning a complete export. Nonfinite bounds are rejected. Blocking HTTP can finish after this cooperative deadline; the late result is rejected and no further page is requested. This is not remote cancellation or a hard wall-clock termination guarantee.

Supply a configured LangChain chat model supporting `bind_tools` to `run_agent`:

```python
from agent_example import run_agent

# model is your configured provider-specific LangChain chat model.
reply = run_agent(
    model,
    "Read the status of my saved task ID: YOUR_TASK_ID. If pending, return the ID for later recovery.",
    api_key=os.environ["TRUEFETCH_API_KEY"],
    max_credits=100,
)
print(reply.content)
```

This defaults to four read/estimate tools. To allow one new collection, supply `allow_write=True, command_mode="new"` and the application's persisted `request_key`; this adds creation and cancellation. The API key must also have the required server-side scope. The loop has bounded steps and calls per step, maps errors into `ToolMessage`, and refuses unknown tools. It does not sleep or repeatedly poll inside model decisions; use deterministic recovery for pending work.

Cancellation is limited to task IDs returned by this session's creation tool. Reading or listing another task never adds it to that scope. When the application explicitly intends to cancel a task recovered after restart, pass `cancel_task_ids=(saved_task_id,)` to `make_tools` or `run_agent`; this application parameter is not exposed in the model's tool schema. Server-side ownership and credential checks still apply. This limits cancellation scope; it does not prove that every model decision within that scope follows the user's intent.

The six tool names match the TrueFetch MCP tool names: `truefetch_get_usage`, `truefetch_estimate`, `truefetch_search_jobs`, `truefetch_get_task`, `truefetch_list_tasks`, and `truefetch_cancel_task`. This adapter uses REST authentication, not MCP OAuth. Get Task includes one saved result page when available; a single tool call is not an all-results export. Follow `meta.pagination.nextCursor`, or use `recover_results` for verified all-page output.

## Limits and evidence

- Model-provider calls may have separate fees. Choose and install your provider integration separately; no provider package or model credential is bundled.
- Listing text is untrusted data. The agent system instruction marks it as such, but this is not proof of prompt-injection resistance. Keep write tools out of downstream text-analysis sessions when they are unnecessary.
- Model prompts, tool arguments and returned records may enter provider requests or LangChain tracing. Configure tracing and retention deliberately. API keys and request keys are not tool arguments; API transport errors omit raw response details.
- Reads reject redirects. Response size is bounded; deterministic recovery stops on unavailable results, incomplete pages, repeated cursors, duplicate IDs or excessive output. It preserves partial status and task metadata. It does not claim source-wide completeness or restore expired results.
- Polling is bounded; each HTTP request can take up to 30 seconds in addition to a polling deadline check. Result pages have an 8 MiB response limit and the assembled export defaults to a 32 MiB serialized bound. Actual memory use may be larger.
- Local tests cover framework schemas, ToolMessage dispatch, budget enforcement, task recovery, two-page output, missing pages, redirect rejection, error redaction, and a scripted model loop. Real model selection, adversarial source text handling, live credentials and source results remain unverified.

Reference: [LangChain tool calling](https://docs.langchain.com/oss/python/langchain/tools).

Recovery checks task identity, terminal status, availability and integer result counts on every page. A changed or malformed response stops recovery without emitting a complete export. This verifies response consistency; it is not a cryptographic proof of upstream data accuracy.

The shared REST reader requires UTF-8 JSON and rejects nonfinite numbers, including numeric overflow. Interrupted or oversized HTTP error bodies keep a redacted HTTP-status diagnostic, and truncated successful transfers fail without returning data. It also rejects duplicate member names within an object, including names written with equivalent Unicode escapes; repeated names in separate records are allowed. Recovery also rejects nonfinite values from tool output instead of marking the export complete. Preserve the saved task ID and original request key after an error; these checks do not create or retry collection.

For each available result page, the reader checks the task again after receiving the page, including the final page. This adds one read-only task GET per page. Changed identity, status, availability or count, or a failed recheck, prevents a complete export. The check is a point-in-time observation; files can expire or become unavailable afterward.

Before collection, the creation tool checks that the estimate matches the requested input and that reserved billing units equal the record limit multiplied by the unit price. Amounts must be positive ASCII integers, and the reservation must fit the host credit budget. Invalid estimates stop before submission; server-side pricing and budget checks still apply.
