Skip to content

test(model-onboarding): cover chat, tool calls, file processing and IS activity tools - #1009

Open
denispetre wants to merge 32 commits into
mainfrom
feat/model-onboarding-agents
Open

test(model-onboarding): cover chat, tool calls, file processing and IS activity tools#1009
denispetre wants to merge 32 commits into
mainfrom
feat/model-onboarding-agents

Conversation

@denispetre

@denispetre denispetre commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

What

The model-onboarding testcase now runs four capability payloads for every get_chat_model code path, all rolled into the single success boolean:

payload what it checks
simple plain ainvoke → non-empty completion
tools full tool-calling round trip (get_weather): the tool is executed and the final answer must use its result
files/<name> the coded file-processing agent (Studio Web "Clone as Coded Agent" of the low-code FileProcessingAgent): production ReAct stack (create_agent + Analyze Files internal tool), test file uploaded as a real platform attachment. When the org forbids attachment creation (403 for the CI principal), degrades to the direct multimodal invoke with an explicit marker
is_tools call-only Activity-tool check: bind the coded copy's real IS tools (Slack Send Message to Channel, Outlook 365 Send Email) and assert the model returns a well-formed tool call — known bound name + required args. Tools are never executed, nothing is sent, no connections needed

API flavor as input

paths entries (workflow input or input.json) accept two forms, freely mixed:

  • registry keys: azure_responses, azure_chat_completions, vertex, bedrock_converse, bedrock_invoke, anthropic_sdk
  • vendor_type:api_flavor pairs passed straight to get_chat_model (exact enum values), e.g. awsbedrock:converse, openai:responses, vertexai:generate-content, awsbedrock:AnthropicMessages. vendor: alone autodetects the flavor.

Every payload — including the coded agents — runs with the model built for that path/flavor.

Design

  • Agents live under src/agents/, one subfolder per coded agent (src/agents/README.md).
  • Each agent.py is the Studio Web generated main.py (downloaded via uip agent file get) with exactly one structural change: the hardcoded llm = get_chat_model('gpt-5.4', ...) becomes build_graph(llm, ...) / build_tools(...) so the testcase injects the model from model_spec — model + API flavor stay configurable; run.sh / input.json / dispatch contract unchanged.
  • Provenance (alpha org llm_gateway_automated_testing): FileProcessingAgent (solution 1823d2d6-…, coded copy e7b8598c-…), IsToolsAgent (solution 925fc5db-…, coded copy 0959f23b-…).
  • The vertex registry path pins vendor_type + api_flavor=GENERATE_CONTENT explicitly (was autodetect-only).
  • Workflow fix: the dispatch input builder now uses python3 — the uv container does not ship jq (first live dispatch failed with exit 127).

Live proof (dispatched via model_onboarding.yml on this branch)

Run 30250006856 — registry path, and run 30277990899 — flavor-pair path (openai:chat-completions), both success on alpha with gpt-5.2-2025-12-11:

simple:      ✓
tools:       ✓
files/*:     ✓ (direct multimodal; attachments unavailable in CI org)
is_tools:    ✓ (Send_Message_to_Channel, Send_Email)

Supersedes #1007. No src/ changes — test + workflow only.

🤖 Generated with Claude Code

… agents

Every code path now runs four capability payloads, rolled into the single
success boolean:

- simple: plain ainvoke, non-empty completion
- tools: full tool-calling round trip (get_weather; executes the tool and
  asserts the final answer uses the result)
- files/<name>: the coded file-processing agent — Studio Web's "Clone as
  Coded Agent" of the low-code FileProcessingAgent — run through the
  production ReAct stack (create_agent + Analyze Files internal tool) with
  the test file uploaded as a real platform attachment
- is_tools: call-only Activity-tool check — bind the coded copy's real IS
  tools (Slack Send Message to Channel, Outlook 365 Send Email) and assert
  the model produces a well-formed tool call (known bound name + required
  args). Tools are never executed; nothing is sent; no connections needed.

Agents live under src/agents/, one subfolder per coded agent. Each agent.py
is the Studio Web generated main.py with exactly one structural change: the
hardcoded llm becomes build_graph(llm, ...)/build_tools(...) so the model
built from model_spec is injected — model + API flavor stay configurable.
The vertex path pins vendor_type + api_flavor=GENERATE_CONTENT explicitly.
utils.py is the generated interpolation helper, verbatim.

Verified: graph compiles, input.json validates, both agents construct, the
call-only assertion covers well-formed/missing-args/unknown-tool/no-call,
and `uipath init` loads the graph.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 24, 2026 15:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

denispetre and others added 2 commits July 27, 2026 11:18
…acks jq

First live dispatch failed with exit 127: the uv:python3.12-bookworm
container does not ship jq. python3 is guaranteed in the image; same JSON
shape, inputs still validated against the testcase's GraphInput.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Live dispatch surfaced it: the helper lives in uipath._utils._ssl_context,
not uipath_langchain._utils. files/* cells now load.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@denispetre denispetre changed the title test(model-onboarding): four payloads per path, Studio Web coded-copy agents test(model-onboarding): cover chat, tool calls, file processing and IS activity tools Jul 27, 2026
denispetre and others added 2 commits July 27, 2026 11:25
…l errors

60 chars hid the real failure in live dispatch diagnostics; cells now carry
the exception type and up to 220 chars.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ts are forbidden

Live dispatch showed 403 on POST /odata/Attachments for the CI principal.
The files cells now try the full generated-agent path (platform attachment +
Analyze Files) and degrade to the proven direct multimodal invoke with an
explicit marker when attachment creation is denied.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@denispetre

Copy link
Copy Markdown
Contributor Author

Live proof via the dispatch workflowmodel_onboarding.yml invoked on this branch (gpt-5.2-2025-12-11, azure_responses, alpha): run 30250006856success.

simple:      ✓
tools:       ✓
files/image: ✓ (direct multimodal; attachments unavailable: EnrichedException)
files/pdf:   ✓ (direct multimodal; attachments unavailable: EnrichedException)
is_tools:    ✓ (Send_Message_to_Channel, Send_Email)

Dispatching also surfaced and fixed three real issues now in this PR: the workflow's input builder used jq, which the uv container doesn't ship (exit 127 → rewritten in python3); a wrong get_httpx_client_kwargs import path; and the CI principal gets 403 on Orchestrator attachment creation, so the files cells degrade to the direct multimodal call with an explicit marker (orgs with attachment permissions get the full generated-agent + Analyze Files path).

paths entries can now be 'vendor_type:api_flavor' pairs passed straight to
get_chat_model (which accepts them as strings) alongside the registry keys —
any API flavor can be supplied directly, and every payload including the
coded agents runs with the model built for it. 'vendor:' alone lets the
factory autodetect the flavor. Workflow paths description documents both
forms.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@denispetre

Copy link
Copy Markdown
Contributor Author

Added: forgiving string coercion in get_chat_model (commit aa36733, folded in from #1011 per review preference).

vendor_type / api_flavor / routing_mode strings are now matched case-insensitively against enum names and values with -/_ interchangeable — so workflow inputs like paths="awsbedrock:anthropic_messages" or "AWSBedrock:Converse" resolve instead of failing on exact spelling; unknown strings raise an immediate ValueError listing valid values. 23 new tests in tests/chat/test_chat_model_factory_coercion.py; enum/None inputs pass through unchanged.

@denispetre
denispetre force-pushed the feat/model-onboarding-agents branch from aa36733 to e0abde2 Compare July 28, 2026 06:50
@denispetre

Copy link
Copy Markdown
Contributor Author

Removed the get_chat_model string-coercion commit per review (aa36733 dropped, force-pushed — the earlier comment about it is obsolete). The PR no longer touches src/; the vendor_type:api_flavor paths input works as strict pass-through with exact enum values, as documented in the workflow's paths description.

denispetre and others added 2 commits July 28, 2026 15:08
…odels

claude-opus-5 (vertex path) burns output budget on adaptive-thinking blocks
before emitting tool-call JSON; at max_tokens=200 the is_tools cell fails
with truncated args (stop_reason=max_tokens, args cut mid-object) while the
identical call at 2000 produces both tool calls fully formed (verified
against alpha). Raised uniformly across all path builders - non-thinking
models are unaffected (they stop early), and 2000 keeps the run cheap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Vertex-hosted Anthropic models (claude-opus-5 is the first through this
testcase) resolve to UiPathChatAnthropic in the client factory; the trace
assertion's span-name alternation only listed the five previously exercised
client classes, failing an otherwise fully green run (all payload cells
passed on alpha + staging).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread testcases/model-onboarding/src/main.py Outdated
except Exception as e: # model construction itself can fail
logger.error(f" construction failed: {e}")
model_results[path] = {"__build__": f"✗ {str(e)[:60]}"}
model_results[path] = {"__build__": f"✗ {f"{type(e).__name__}: {e}"[:220]}"}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think f-string nested inside the other f-string with the same quotes is a problem. looks like this pattern became legal in pyrthon 3.12 but pyproject says requires-python = ">=3.11" so i suppose if you try running it on 3.11 it might break. I suppose CI uses a newer version so thats why it goes green? If u plan on changing it it also happens on line 372, 380, 392 and 404. link to this: https://peps.python.org/pep-0701/

denispetre and others added 13 commits July 29, 2026 16:56
Replaces the multi-payload grid (simple/tools/files/is_tools) with a single
probe over the coded file_processing agent, and drops the is_tools agent and
the AGENT_REGISTRY indirection that only existed to multiplex agents.

The probe now records what the model actually returned: run() yields the
model's own answer prefixed with the route it took ([agent] via a real
platform attachment + Analyze Files, or [direct] when the org forbids
attachment creation) and raises on empty output instead of collapsing
everything to a bare tick.

expected_traces.json follows the node rename (run_model_onboarding ->
probe_file_processing). paths keeps both shorthands and vendor:flavor pairs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
.agent/, AGENTS.md, CLAUDE.md, agent.mermaid, bindings.json,
entry-points.json and project.uiproj are regenerated by `uipath init` in
run.sh; no sibling testcase tracks them. Swept in by an over-broad git add.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… output

The live run showed the Responses API returns content as a list of block
dicts, so the probe recorded the whole raw structure including a per-response
'id' that changes every run. Extract just the text so results are readable
and deterministic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The field named code paths but its values are api flavors, so rename it
end to end: model_spec.api_flavors, FLAVOR_SHORTHANDS, the workflow's
api_flavors dispatch input, and the docs. Both value forms are unchanged
(shorthands and vendor_type:api_flavor pairs).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Keeps only what running the agent needs: the file registry, the model spec,
one probe node that builds the model per api_flavor and calls the agent per
file, and the graph the runtime entrypoint requires. Drops the second graph
node, the GraphState dict, the flavor shorthand map (vendor_type:api_flavor
is the general form), and the defensive guards around settings/model build/
unknown files. The one try/except left wraps the agent call so a model
failure yields success=False instead of aborting the run.

205 -> 116 lines.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The runtime recompiles the graph with its own checkpointer (runtime/factory.py
_compile_graph unwraps .builder), so the local .compile() result was discarded.
Drop build_graph() and the redundant compile, build the StateGraph at module
level, and let add_node infer the node name from the function.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Falling back to a plain multimodal call when attachment creation is denied
reported a pass for a run that never exercised the agent path — the false
confidence this testcase exists to catch. Let the failure surface: the cell
records it and success goes false.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A non-empty answer proved little — a model can describe 'a PDF document' from
the file name alone. Two checks now establish the file was read:

- FILE_EVIDENCE: the answer must contain words only the contents reveal (the
  dummy PDF's literal 'Dummy PDF' text; village + sea for the photo), else
  the cell fails with what was missing and what came back.
- expected_traces.json now requires an 'Analyze Files' TOOL span, so the run
  fails if the agent never invoked the file-reading tool.

Also collapse multi-line SDK errors onto one line so failing cells stay
readable (the 403 detail was rendering across newlines).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Keyword matching on a descriptive answer was fuzzy and could pass on a lucky
guess. Each file now carries its own question with a single correct word that
only its contents reveal, and the file names are neutral so the answer cannot
leak from them:

- animal.jpg (a photo of a dog) -> 'What animal is in this image? Answer with
  one word only.' -> must contain 'dog'
- document.pdf (content: 'Dummy PDF file') -> 'What is the first word of the
  text inside this document?' -> must contain 'dummy'

FILE_REGISTRY entries become FileCase(file, question, expected); the global
prompt input is gone from GraphInput, input.json and the workflow, since each
file asks its own question. Docs now describe only the vendor_type:api_flavor
form (the shorthand map was removed earlier).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The files knob was near-useless (you would rarely skip a format) and had to be
kept in sync across the workflow input, ModelSpec and input.json. The probe now
iterates FILE_REGISTRY directly, so adding a format is a single registry entry.

Dispatch inputs are now model_name / api_flavors / agenthub_config /
environments.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AGENTS.md, CLAUDE.md, .agent/, agent.mermaid and project.uiproj are
regenerated by `uipath init` in run.sh and kept sweeping into commits.
entry-points.json/bindings.json are deliberately left tracked because
debug-breakpoints and dev-console commit theirs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

denispetre and others added 11 commits July 29, 2026 17:41
Attachment creation is folder-scoped in Orchestrator (the candidate
permissions BLOBFILES/EXECUTIONMEDIA/JOBS are all ScopeType=FOLDER), but the
upload was called with no folder at all, which may be why it 403s rather than
a missing grant. Pass folder_path from UIPATH_FOLDER_PATH and set that to
'Shared' in the dispatch workflow to find out.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Attachment creation 403s and the client id cannot be recovered (GitHub
secrets are write-only and masked; APIM App Insights logs no caller
identity). Decode the non-secret JWT claims after auth (sub / client_id /
org / tenant / scope) so the principal's Orchestrator permissions can be
checked. Prints no token and no secret; whitelisted claims only.

Remove once the 403 is resolved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Root cause of the attachment 403: `uipath auth --scope` defaults to
'OR.Execution' alone, so the S2S token carried exactly that one scope even
though the external app (UiPath-Python-Integration-Tests in agenthubdri)
grants all 73 Orchestrator scopes. POST /odata/Attachments was rejected at the
OAuth scope layer, which is why no role or permission change had any effect.

Request OR.Jobs (attachments are job artifacts), OR.Administration and
OR.Folders alongside OR.Execution.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e success gate

The 'Analyze Files' TOOL span I asserted does not exist: the runtime emits
AGENT/CHAIN/LLM kinds only and internal tools execute inside the agent span.
The tool call itself is visible in the LLM span output
(tool_calls: [{name: Analyze_Files, args: {attachments: [...]}}]).

Assert what the traces actually contain and what proves the file was read:
attachments_upload, attachments_get_blob_uri (the blob was fetched), the agent
AGENT span, plus the existing probe CHAIN and LLM spans.

Also move assert_traces above the success assert. It previously sat after it,
so on any failing run the trace checks never executed and the trace dump was
never printed - which is precisely when they are needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
It served its purpose: the token claims showed scope=['OR.Execution'], which
led to the real fix (requesting the needed scopes in run.sh). No longer needed
now that attachment creation succeeds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
One run failed with the agent raising AgentRuntimeError while the tool had in
fact returned 'Dummy': asking for 'the first word of the text inside' invited
reasoning about extraction quality rather than an answer. Ask what the document
says instead. (Same question passed in two earlier runs, so this is phrasing
flakiness, not a broken path.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The last run passed the pdf cell on 'Unable to reliably read the PDF contents:
the analysis tool returned a non-informative placeholder result (Dummy), so
the document's text cannot be determined.' — a substring check cannot tell a
real answer from a hedge that quotes the word while asserting the opposite.

Each question asks for one word, so require the expected word as an actual
token AND an answer of at most four words. Verified against nine cases
including that exact hedge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Asking what the document 'says' let the model judge whether 'Dummy' was real
content or a placeholder; it repeatedly reported the file unreadable while
quoting the exact word the tool had returned. Asking for a transcription
('three words long') removes that judgement. Still within the 4-word answer
limit, so the strict matcher applies unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
dummy.pdf's content is the literal string 'Dummy PDF file', which reads as a
placeholder: across six runs the model answered correctly three times and three
times refused, reporting the file unreadable while quoting the text the tool
had returned. No PDF library is available here to verify the text of an
alternative public file, so keep the known file and instruct the model to
repeat the tool output verbatim without evaluating it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 4-word limit rejected a correct answer: obeying 'verbatim' the model
returned '<PARSED TEXT FOR PAGE: 1 / 1> Dummy PDF file', where the prefix is
the platform parser's own marker. Drop the length rule and reject refusal
phrasing instead ('cannot', 'placeholder', 'unable', 'no actual', ...), which
is what actually distinguishes an answer from a hedge that quotes the word.

Validated against every answer observed in the live runs: four genuine answers
accepted (including the parser-prefixed one) and three real refusals rejected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

# Phrases a model uses when it quotes the expected word while denying it read
# the file. A plain substring check passed those, asserting the opposite of
# what the answer said.
_REFUSAL_MARKERS = (

@tudormatei1 tudormatei1 Jul 30, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these seem quite flaky no? refusal can be stated in many ways, i dont think its reliable to test like this.

my idea to test would be to have a simple token in the pdf like a code lets say: 12345, now theres no way the model can just guess it or say the file is a placeholder, it either read it or it didnt.

and then you can just do an exact match and drop the marker list entirely (cause you just check for code).

for images its a bit different i think cause its quite unfair to make it read a code from the image, so maybe use something that is less probable to be guessed (in this case dog is the first thing that comes to mind so it might just say dog without looking at the image)

one example: have a purple duck image and ask what color it is

"image": FileCase(
# A white Samoyed sitting on grass.
file=FileInfo(
url="https://raw.githubusercontent.com/pytorch/hub/master/images/dog.jpg",

@tudormatei1 tudormatei1 Jul 30, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

im not really sure about this but does the file name in this case the "dog.jpg" create a bias? maybe the model cannot open the file and just says dog.jpg because of the filename? i believe the model has direct access to the file name inside:

return [
        SystemMessage(content=enhanced_system_prompt),
        HumanMessage(content=interpolate_legacy_message("""{{prompt}}

{{fileIn}}""", state.model_dump())),
    ]

this can be solved with the comment from below i guess

@tudormatei1

Copy link
Copy Markdown
Contributor

im not sure about this but i though we talked yesterday to only include one simple tool call like you had with get_weather_data (or whatever it was called)?

we had that discussion with either keeping one simple tool call test like that or have a really difficult and complicated one. right now we only test the analyse file tool maybe thats enough?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants