Skip to main content

Build a configurable ReAct agent

What you'll build: one web-research agent that runs two different ways depending on a single string you pass at call time — quick, a fast, shallow-search persona on a cheap model, or deep, a thorough, wide-search persona on a stronger model. Same prompt, same tool, same code. The only thing that changes between the two is which alias you render.

Here's the full flow:

  1. Create the prompt with two versions — v1 (quick persona + cheap model) and v2 (deep persona + stronger model).
  2. Create aliases — point quick at v1 and deep at v2 using the Versions tab's "New alias" form.
  3. Render by alias — your code calls render_prompt("web-research-agent", "quick") or render_prompt("web-research-agent", "deep"), and the platform returns that version's model, system prompt, and tools.
  4. Run the loop — the SDK drives the gateway completion loop with whatever the render returned. The model, the persona, and the search depth all changed because the alias changed.

An alias is a named pointer at a specific version — production and staging are the two AcruxCore creates for you automatically, but an alias is just a string, not a fixed enum. Nothing stops you from pointing others at whatever versions you want: quick, deep, a customer id, a region. Swapping which alias your code renders is the entire configuration mechanism: no redeploy, no code change, not even a restart.

Prefer a notebook?

web_research_agent.ipynb is this whole page as one runnable notebook, written for a first-timer: a preflight cell that checks both models and the Tavily key before anything else, both prompt versions and both aliases created step by step, the two runs read back from the trace API to prove which model really answered, and four ways to get it wrong triggered on purpose — including the big one, where committing a new version moves no alias and every caller keeps the old behaviour. It renders on GitHub with its saved output, so you can read it through before running anything.

What you'll need

A Tavily API key (the free tier is enough). The Python tab uses Tavily's own SDK (pip install tavily-python); the Node tab uses npm install @langchain/tavily.

1. See the shallow vs. deep search for yourself

The source this page reproduces defines two configurations of the same Tavily search — basic_research (5 results, basic depth, includes images) and advanced_research (10 results, advanced depth):

from tavily import AsyncTavilyClient

async def advanced_research(query: str):
"""Deep research: 10 results, advanced depth."""
res = await AsyncTavilyClient().search(query, max_results=10, search_depth="advanced")
return res["results"]

async def basic_research(query: str):
"""Quick research: 5 results, basic depth, includes images."""
res = await AsyncTavilyClient().search(
f"trending {query}", max_results=5, search_depth="basic", include_images=True
)
return res["results"]

AsyncTavilyClient reads TAVILY_API_KEY from the environment, and search() answers {"query", "results", "images", ...} — so the results are one level in.

Both configurations are one real call to Tavily's REST API (https://api.tavily.com/search) with different parameters. Run them standalone first, before any of this touches AcruxCore, to see the two depths visibly differ:

curl -s -X POST https://api.tavily.com/search \
-H "Content-Type: application/json" \
-d '{"api_key":"'"$TAVILY_API_KEY"'","query":"AcruxCore LLM gateway pricing","search_depth":"advanced","max_results":10}' \
| python3 -c "import json,sys; d=json.load(sys.stdin); print('results:', len(d['results'])); print('images:', len(d.get('images', [])))"
results: 10
images: 0
curl -s -X POST https://api.tavily.com/search \
-H "Content-Type: application/json" \
-d '{"api_key":"'"$TAVILY_API_KEY"'","query":"trending AI agent frameworks","search_depth":"basic","max_results":5,"include_images":true,"include_raw_content":false}' \
| python3 -c "import json,sys; d=json.load(sys.stdin); print('results:', len(d['results'])); print('images:', len(d.get('images', [])))"
results: 5
images: 4

10 results with no images vs. 5 results with 4 real image URLs attached — the same API, visibly different depth, driven entirely by the request body. That's the axis this whole tutorial turns into a runtime config swap. This standalone check calls the raw REST API directly (curl has no LangChain to import), specifically to show the image count difference: the source's own TavilySearchResults wrapper — the one the Python tab below calls directly — passes include_images=True through to this same endpoint, but its plain .ainvoke() return only surfaces title, url, content, and score, dropping images even when they're requested. That's a real, narrow quirk of that one deprecated Python class, not of Tavily wrappers in general — the Node tab's TavilySearch (from @langchain/tavily, the actively maintained successor to TavilySearchResults) returns a real images array from the same kind of call, confirmed when building the Node tab below.

2. Get an API key

The scripts below need one AcruxCore key to create the prompt and tool, render each alias, and run the loop. Open Account & keys → New key, name it something like configurable-react-agent, and copy the value — it's shown once.

The "Copy your API key" dialog showing a freshly created key beginning acx_sk_, a Copy button, and a Node SDK snippet using it

export ACRUXCORE_API_KEY=acx_sk_...
export ACRUXCORE_BASE_URL=https://api.acruxcore.com/api/v1
export TAVILY_API_KEY=tvly-...

3. Create the web_research tool

Open Gateway → Tools, click New tool, and name it web_research. Its schema is deliberately thin — the model only ever supplies a query; which depth that query runs at is decided by which alias is active, not by the model.

New tool dialog with the name web_research and a description about searching the live web

Click Create tool, then New version to define its one parameter and pick Client — the caller's app runs it: the platform stores the schema, your own code runs the real Tavily call.

New version form showing a required query string parameter and the executor set to Client

Click Commit version. web_research now exists with one parameter and no opinion at all about search depth — that lives entirely in the prompt versions you're about to create.

4. Create the prompt: two versions, two personas

Open Prompts → New prompt, name it web-research-agent.

New prompt dialog with the name web-research-agent and a description about the configurable persona

On the Editor tab, set Default model to gemini-flash — a small, cheap, fast model — and write the quick persona's system message. Add a user message of {{ question }} so the actual question is supplied at render time, not baked into the template:

Editor tab with default model gemini-flash and a system message describing the fast, shallow-search persona

Click Commit new version. This is v1 — production and staging both auto-point at it, since it's the first version.

Now switch to the Tools tab and use + Connect a tool from the catalog to pick web_research. It saves straight away, in the default column, so every alias of this prompt calls it — including the two you are about to create.

Tools tab with web_research connected in the default column

Back on the Editor tab, change Default model to claude-haiku — a stronger model — and replace the system message with the deep persona's framing: more thorough, more sources, more synthesis. The tool wiring is not part of a version, so web_research stays connected whatever you commit. Click Commit new version again to create v2:

Editor tab now showing default model claude-haiku and a system message describing the thorough, deep-search persona

The Versions tab now shows both: v1 on gemini-flash with production and staging both still pointing at it, v2 on claude-haiku with neither alias moved yet.

Versions tab listing v2 on claude-haiku and v1 on gemini-flash, with production and staging both still on v1

5. Point quick and deep at each version

An alias is a named pointer at a specific version. When your code calls render_prompt("web-research-agent", "quick"), the platform looks up which version quick points at, and returns that version's model, system prompt, and tools. production and staging are the two aliases AcruxCore creates automatically on the first version — but an alias is just a string, not a fixed enum. You can create any name you want.

Here's what we're setting up:

AliasPoints atModelSystem prompt
quickv1gemini-flashFast, concise persona
deepv2claude-haikuThorough, multi-source persona
productionv1gemini-flashFast, concise persona
stagingv1gemini-flashFast, concise persona

The only difference between a quick run and a deep run is which alias you pass — the model, the persona, and the search depth all come from whichever version that alias points at.

Creating the aliases

Open the Versions tab. Each version row shows which aliases currently point at it, and each row has promote buttons (→ production, → staging, etc.) that move an existing alias to that version. But we need new names, so scroll to the bottom of the tab to find the New alias form.

Type quick in the alias name field, select v1 from the version dropdown, and click Promote. The upsert creates the alias — promoting a name that doesn't exist yet simply creates it.

New alias form with quick typed in the name field, v1 selected in the version dropdown, and the Promote button enabled

Do the same for deep: type deep, select v2, click Promote.

What changed

Both custom aliases now appear as badges on their respective version rows. On the v1 row, you'll see PRODUCTION, STAGING, and QUICK badges — all three point at v1. On the v2 row, the DEEP badge sits alone. Custom aliases show a × delete button; production and staging cannot be deleted.

The prompt header also displays all four alias badges with their version pointers — this is the at-a-glance view of what's bound where:

Versions tab showing v2 with a deep badge and v1 with production, staging, and quick badges, plus the header showing all four alias pointers

How aliases are used

Aliases show up in three places:

1. The Playground. Open Gateway → Playground → Stored prompt, pick web-research-agent, and the Alias dropdown lists all four. Selecting deep · v2 instantly swaps the system message to the thorough persona — proof that the alias resolved to v2's content:

Playground's Stored prompt tab with web-research-agent selected and the Alias dropdown showing production v1, staging v1, quick v1, and deep v2, with deep selected and the system message updated to match

2. The render API. When your code calls the render endpoint, the alias determines everything:

# This returns v1's model + system prompt (quick → v1)
curl -s -X POST "$ACRUXCORE_BASE_URL/prompts/web-research-agent/quick/render" \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" -H "Content-Type: application/json" \
-d '{"variables":{"question":"What is new in AI?"}}'
# -> { "model": "gemini-flash", "messages": [...fast persona...], "tools": [...] }

# This returns v2's model + system prompt (deep → v2)
curl -s -X POST "$ACRUXCORE_BASE_URL/prompts/web-research-agent/deep/render" \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" -H "Content-Type: application/json" \
-d '{"variables":{"question":"What is new in AI?"}}'
# -> { "model": "claude-haiku", "messages": [...thorough persona...], "tools": [...] }

Same endpoint, same variables — the alias on the URL is the only thing that changed. The model, system prompt, and tool list all came from whichever version that alias points at.

3. Your code's tool function. The one place your code does branch on the alias is the web_research implementation — because search depth is a client decision (step 3 deliberately left it out of the tool schema). The function picks Tavily's search_depth and max_results to match the alias:

async def web_research(query: str, alias: str) -> list:
if alias == "quick":
wrapped = TavilySearchResults(max_results=5, search_depth="basic", ...)
else:
wrapped = TavilySearchResults(max_results=10, search_depth="advanced")
return await wrapped.ainvoke({"query": query})

This is the only if alias == in the entire script. The model, the persona, and the system prompt all came from the render call — only the tool configuration branches, because it lives on the client side.

Aliases via curl

The UI is not the only way — the API accepts any string: POST /prompts/:id/aliases/:alias/promote with {"version_number": N}. This is useful for automation or CI pipelines that need to create aliases programmatically.

6. Run the agent

As step 5 explained, the alias determines everything — model, system prompt, and tools all come from whichever version that alias points at. The only thing that changes between a quick run and a deep run is the alias string you pass.

The one place the scripts do branch on the alias is the web_research implementation, which picks Tavily's search_depth/max_results to match. This is because search depth is a client-side decision — step 3 deliberately left it out of the tool schema, so the function carries that config instead. Both scripts build their client_tools map per alias for exactly that reason: the closure is where the alias-dependent configuration lives.

Because web_research is a client tool, the gateway can't run it — your code has to. Python and Node use the SDK's run_prompt_with_tools()/runPromptWithTools() to drive the back-and-forth; curl has no such helper, so its loop is written by hand, the same shape Build a tool-calling agent in Python (no SDK) uses: a header trick (x-trace-name to open a trace, x-gateway-trace-id back, then x-trace-id on every later call) keeps the whole run in one trace, since the gateway itself records every llm span now — unlike Build a ReAct agent's BYO path, nothing here has to be reported manually except the tool spans web_research produces.

The three tabs differ only in how they reach Tavily, and all three reach the same endpoint. curl has no way to import a client library, so it calls https://api.tavily.com/search by hand. Python uses tavily-python, Tavily's own maintained SDK. Node uses @langchain/tavily, which is the actively maintained standalone package for it.

None of this changes what the model sees. web_research is one catalog tool with one schema in every tab; only the code behind it differs, and that is the point of a client executor.

The full script loops the render → complete → run the tool cycle by hand with curl and jq, since curl has no run_prompt_with_tools()/runPromptWithTools() equivalent — see web_research_agent.sh on GitHub for the complete source.

Run it once per alias — same script, same flags, only the first argument changes:

./web_research_agent.sh quick "What are people saying about the new Anthropic Claude models?"
Alias: quick -> model gemini-flash
Question: What are people saying about the new Anthropic Claude models?

-> web_research({"query":"Anthropic Claude models reviews"})
{"results":[{"title":"Best Anthropic Models (July 2026) — Ranked by ...","url":"https://benchlm.ai/best/anthropic-models"},{"title":"Anthropic Review 2026: Is the Claude API Really Worth It? | Hack'celeration","url":"https://hackceleration.com/labs/review/anthropic"},{"title":"Claude 4 Initia
Assistant:
Early reviews suggest that Anthropic's Claude models, particularly the Claude 3 family, are highly capable, with some users praising their performance and abilities. Developers have noted specific strengths, such as Claude's potential for code review and generation. The models are generally seen as strong competitors in the AI landscape.

(2 model turn(s), trace cd90701b-c820-42c5-aa42-56a6437265ca)
./web_research_agent.sh deep "What are people saying about the new Anthropic Claude models?"
Alias: deep -> model claude-haiku
Question: What are people saying about the new Anthropic Claude models?

-> web_research({"query":"Anthropic Claude models 2023 reviews opinions reactions"})
{"results":[{"title":"Claude Opus 4.7 Review: Every Anthropic Claude Model Explained","url":"https://webwallah.in/claude-opus-4-7-anthropic-models-complete-guide"},{"title":"AI Governance and Accountability: An Analysis of Anthropic's Claude","url":"https://arxiv.org/html/2407.01557v1"},{"title"
Assistant: The new Claude models from Anthropic have generated a variety of discussions and opinions across media platforms, highlighting both their capabilities and limitations. Here are some of the notable points of feedback:

1. **Claude Opus Models**: An article by Webwallah provides an overview of the Claude Opus 4.7 model, explaining its features and improvements over earlier iterations. Users have noted the enhanced conversational abilities and the focus on maintaining less biased outputs compared to prior models.

2. **AI Governance Concerns**: A research paper on arXiv discusses the implications of Anthropic's Claude models in terms of AI governance and accountability. It highlights some ethical concerns surrounding AI responses and the need for transparent guidelines in AI usage, pointing out that as powerful as these models are, they require careful oversight.

3. **Performance Analysis**: Some users have reported that Claude's ability to maintain depth in conversation seems to have diminished as model iterations have progressed. A Reddit discussion indicates that Claude's "thinking depth" dropped by 67%, which has raised concerns about consistency in providing coherent and insightful responses.

4. **Comparisons with ChatGPT**: Various reviews and comparison articles examine Claude's performance against market leaders like ChatGPT. A Scale AI blog discusses how the models handle multi-step reasoning and showcases instances where Claude excels or falls short in direct comparisons, indicating a nuanced rivalry in the space of conversational AI.

5. **Code Review Functionality**: Claude's new capabilities include the ability to review and analyze code, which has been a significant focus for many developers. Video reviews and LinkedIn posts from users discuss their hands-on experiences with Claude Code, emphasizing its potential utility in debugging and code enhancement tasks.

6. **General Sentiments**: Reviews from tech outlets like CNET describe Claude as "the most conversational AI engine," underscoring its advancements in natural language processing and user interaction. However, some users express nuanced opinions, citing instances of misinterpretation or less-than-optimal responses compared to earlier versions.

In summary, while many users appreciate the strides made with the Claude models, particularly in conversation and coding applications, there are ongoing debates about the consistency and reliability of outputs, as well as important discussions about the ethical implications of deploying such powerful AI systems.

(2 model turn(s), trace 2b9fe9a2-27ff-496e-8cac-b7f8fa581a86)

Same script. Same flags. The model, the persona, and the search depth all changed because the first word on the command line did.

7. Inspect the trace

Open Observability → Traces and every run above shows up as web-research-agent — the SDK runs even carry the session id each script passed (web-research-quick/web-research-deep), and every row has a real dollar cost, because these all went through the gateway:

Traces list showing web-research-agent runs, tagged session web-research-quick and web-research-deep, each with real token counts and costs

Open the Node deep run from above and the tree shows the full loop: an llm span, a web_research tool span, and a final llm span with the answer — three spans, under the model this alias bound, claude-haiku:

Trace tree for the Node deep run showing an LLM span, a web_research tool span, and a final LLM span, all on claude-haiku

Click the web_research span to see the real query the model chose and the real Tavily results the real TavilySearch wrapper got back:

Expanded web_research span showing the input query Anthropic Claude models reviews and feedback 2023 and real Tavily result titles and URLs as output

What's next

Alias management

Custom aliases can be created from the Versions tab (as shown above) or via the API (POST /prompts/:id/aliases/:alias/promote). To delete a custom alias, click the × on its badge in the Versions tab. The production and staging aliases are protected and cannot be deleted. In evaluations, the production alias is used as the automatic baseline for experiment comparison — every run compares against whatever version production currently points at.