# Job Search API with LlamaIndex

Preview: Python 3.12 and `llama-index-core==0.14.25`, tested with native FunctionTool calls and a loopback HTTP fixture. No LangChain dependency. Live TrueFetch credentials, upstream results, and real-model selection remain unverified. This REST example works independently of MCP availability; it is not a published LlamaHub integration.

Keep `truefetch_llamaindex.py`, `truefetch_rest.py`, and `requirements.txt` together. In this repository the common engine lives in `examples/python/`; the download kit includes it alongside the adapter. Install in a virtual environment:

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


## 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.


## Dependency security — checked September 30, 2026

The pinned LlamaIndex core package currently resolves `nltk==3.10.3`. NLTK's [model artifact path advisory](https://github.com/nltk/nltk/security/advisories/GHSA-8mgp-746c-j5xp) has no published patched version. This kit uses FunctionTool and FunctionAgent; it does not expose NLTK model import, export or training APIs. Use a separate development environment, do not add tools that let untrusted input choose model file paths, and review a fixed dependency tree before using this kit with production data. This usage boundary does not remove the vulnerable installed dependency.

The application owns credentials, a per-collection budget and one persisted request key. Do not put credentials into model arguments. Persist the key and original input before creating a task. Calling creation reserves credits and may charge them; configure account/API-key monthly limits separately.

```python
import os
from truefetch_llamaindex 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 = {tool.metadata.name: tool for tool in tools}
job_input = {
    "keyword": "Software engineer",
    "country": "Germany",
    "platforms": ["arbeitnow"],
    "maxResults": 10,
}
quote = by_name["truefetch_estimate"].call(input=job_input).raw_output
accepted = by_name["truefetch_search_jobs"].call(input=job_input).raw_output
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("Persist this task ID before continuing:", task_id)
result = recover_results(tools, task_id, timeout_seconds=300)
# result["data"] contains all verified pages; partial remains explicit.
```

The six native tools provide usage, estimation, creation, task/results reads, task listing, and scoped cancellation. Default `allow_write=False` exposes only four read/estimate tools. Cancellation is restricted to tasks created by this factory or `cancel_task_ids` explicitly supplied by the host. Reading a task never grants cancellation scope. Native `call()` returns a ToolOutput; consume its `raw_output` dictionary. Framework asynchronous calls use `await tool.acall(...)`; deterministic recovery is synchronous and should run outside an async event loop or in a worker thread.

Creation refreshes the estimate, checks expiry and reserved units against the fixed budget, then submits the pricing version with the original idempotency key. It does not retry HTTP automatically. If a response is lost, keep the original key and input; do not generate a new intent. If a task ID is already known, instantiate read-only tools and use `recover_results()` directly. Recovery polls a bounded interval, validates count/order across all pages, rejects missing pages and bounds memory. A local timeout does not cancel the remote task.

## Bounded native agent workflow

Recovery checks its deadline before and after each tool read, throughout pagination and before returning a complete export. Nonfinite time bounds are rejected. This is a cooperative deadline: a blocking HTTP call can finish after it, but no late page is returned as a complete export and no subsequent page is requested. It is not remote cancellation or a strict wall-clock termination guarantee.

Keep `agent_example.py` alongside the files above. Supply your configured function-calling LlamaIndex model; this module does not create one or load provider keys:

```python
from agent_example import run_agent, AgentRunError

try:
    reply = await run_agent(
        llm,
        "Read the status of my saved task ID: YOUR_TASK_ID",
        api_key=os.environ["TRUEFETCH_API_KEY"],
        max_credits=100,
        max_steps=8,
        max_tool_calls=16,
        timeout_seconds=120,
    )
    print(reply["task_ids"])
except AgentRunError as error:
    print("Resume known tasks:", error.task_ids)
```

Run this in an async function or notebook. Defaults are read-only. Writes require `allow_write=True, command_mode="new"` and the original persisted request key. The native FunctionAgent uses force stopping rather than an additional model-generated answer at its iteration limit; a separate application counter limits total tool invocations even if a model returns parallel calls. Provider failures produce a fixed error and retain observed handles. The framework counts iterations before handling each response, so reaching the configured maximum can reject the last response. An agent answer is not a verified result export.

Model/provider setup and costs are separate; these limits are not a dollar/token budget. A timeout cannot prove cancellation of an in-flight HTTP call or remote collection. Keep the request key/input even if no task ID was observed. Do not enable hosted tracing or callbacks that send private task data without reviewing their destination. Retrieved source text is untrusted data, never authority to create/cancel jobs or disclose secrets. Local scripted models verify workflow mechanics, not real-model resistance to prompt injection. Keep result recovery outside the model loop.

Run `python -m unittest discover -s examples/llamaindex-job-search -p test_tools.py -v` from the checkout for local regression tests. No tests, environments or private files are included in the public download kit.

Reference: [LlamaIndex FunctionTool API](https://developers.llamaindex.ai/python/framework-api-reference/tools/function/).

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.
