Skip to content

Latest commit

 

History

History
831 lines (682 loc) · 33.5 KB

File metadata and controls

831 lines (682 loc) · 33.5 KB

HttpCalls

Overview

HttpCalls enable EDDI agents to integrate with external REST APIs, making EDDI a powerful orchestration layer that can combine conversational AI with traditional backend services. This is how agents can fetch real-time data, authenticate users, store information in external systems, or trigger business workflows.

Role in the Lifecycle

HttpCalls are lifecycle tasks that execute during the agent's processing pipeline:

User Input → Parser → Behavior Rules → HttpCalls → Output Generation

Typically, Behavior Rules decide when to make an API call by triggering an action like httpcall(weather-api), and the HttpCalls extension defines how to make that call.

Common Use Cases

  • Fetching external data: Weather, stock prices, product information, etc.
  • Authentication: OAuth flows, token validation, user verification
  • CRM Integration: Creating tickets, updating customer records, searching databases
  • Business workflows: Processing payments, sending notifications, triggering events
  • Multi-step APIs: First call gets auth token, second call uses it to access protected resources
  • Analytics: Sending conversation data to external analytics platforms
  • Self-modification: The Platform Operator calls EDDI's own API to create other agents, through configs generated from EDDI's OpenAPI spec by McpApiToolBuilder. Those are the same documents this page describes: the store and URI call them apicalls, the workflow step and file extension say httpcalls (see Agent Config Authoring)

Key Features

  • Template-based: Use conversation memory in URLs, headers, and body (e.g., {context.userName})
  • Response handling: Save JSON responses to memory for use in outputs or subsequent calls
  • Chaining: One HttpCall's response can be used in another HttpCall
  • Quick reply generation: Automatically create quick reply buttons from API response arrays
  • Property extraction: Extract specific values from responses and save them to conversation memory
  • Batch requests: Make multiple API calls by iterating over an array (requires "fireAndForget": true)
  • Fire and forget: Optional asynchronous calls that don't wait for a response
  • Caller identity: Call an API as the signed-in user with ${caller:token} — see Calling as the signed-in user

Calling as the signed-in user

Since 6.2.0.

A header can reference the authenticated caller, so the agent calls an API with that user's credentials rather than one static credential baked into the config:

Reference Resolves to
${caller:token} The caller's raw bearer token
${caller:userId} The caller's principal name (not a secret)
"headers": {
  "Authorization": "Bearer ${caller:token}"
}

This matters most when the API being called is EDDI's own. A static credential is the wrong shape there: an OIDC token expires within the hour, cannot be least-privilege, and collapses every action to a single synthetic principal in the audit trail. With ${caller:token}, authorization stays EDDI's normal per-endpoint enforcement and the audit trail names a real person. This is what the EDDI-Manager Platform Operator uses.

Rules

Resolution is deliberately narrow, and each rule fails the call loudly rather than degrading quietly:

  • Same origin, or EDDI itself. The token is released only when the call targets the exact scheme://host:port the caller addressed, or this deployment's own address. The caller's origin is read from the inbound request; the deployment's own address comes from deployment configuration alone — eddi.self.base-url, otherwise http://127.0.0.1:${quarkus.http.port} (or the quarkus.http.host address, when the listener binds one specific non-loopback address and 127.0.0.1 is therefore not this process) — never from an agent config or a request header. So a config naming a third-party host cannot exfiltrate a user's token, and no allow-list is needed for this to be safe by default. With quarkus.http.port=0 (a random port) and no eddi.self.base-url, the self address is unresolved: nothing is derived, and the token is released to the caller's origin only — set eddi.self.base-url or a fixed port for the self release to apply.

    The second half exists for the Platform Operator, whose tools must call EDDI at an address the server can reach, which behind a tunnel, a port mapping or a reverse proxy is not the address the browser used. Be aware of what it means: the self address bypasses whatever sits in front of EDDI, so any agent that targets it with ${caller:token} reaches exactly what EDDI's own authorization allows that user, not what the proxy would also permit. If a deployment relies on its proxy for path or network restrictions on EDDI's API, set eddi.caller-identity.self-release.enabled=false. A turn whose caller origin could not be captured never gets the self release.

    With eddi.security.ssrf-protection.enabled=true, the loopback default is refused before any request is made (loopback is exactly what that protection blocks), so such a deployment must set eddi.self.base-url to an address the server can reach and that passes the full SSRF target policy — which also rejects private and link-local addresses, so an in-cluster service name that resolves to a private IP stays blocked. eddi.self.base-url must be a bare scheme://host[:port]; a value with a path, query, fragment or credentials is ignored (logged at ERROR). A plain-http value naming a non-loopback host is accepted but logged at WARN, since the caller's token would cross the network unencrypted unless a mesh or network layer protects it.

  • Headers only. ${caller:token} in a query parameter, request body or request path is rejected. Tokens in URLs leak through access logs, proxies and browser history, and nothing outside a header is substituted anyway — a reference there would travel to the API as literal text. ${caller:userId} may be used in headers and query parameters.

  • Authenticated turns only. The identity comes from the request that drove the turn, so scheduled jobs and triggers cannot satisfy ${caller:token}.

  • Fails closed. An unsatisfiable reference raises an error instead of resolving to an empty string, which would send Bearer and surface later as a confusing 401.

The resolved token is never written to conversation memory: authorization headers are scrubbed before the request is recorded.

Set eddi.caller-identity.enabled=false to forbid the feature outright.

The same reference works in an MCP server's apiKey, so a tool call reaches that server as the chatting user rather than as a standing service principal. Only the tool call carries the caller — the handshake and tools/list do not, because the client is cached and a session opened with one user's token would be reused by everyone after them. See mcp-server.md.

Running behind a reverse proxy

The origin is taken from the inbound request as EDDI sees it. Behind a TLS-terminating proxy or ingress, that is the internal hop — something like http://10.0.0.5:8080 — while the caller addressed https://eddi.example. The two do not match, so ${caller:token} fails closed and the agent reports that the call targets a different origin, with nothing obviously wrong in the config.

If EDDI runs behind a proxy, enable forwarded-header handling so the request reflects what the caller actually addressed:

quarkus.http.proxy.proxy-address-forwarding=true
quarkus.http.proxy.enable-forwarded-host=true

This is deliberately not on by default. It makes EDDI trust X-Forwarded-* headers, which any client can send — safe when a trusted proxy overwrites them, wrong when EDDI is directly reachable. Turn it on only together with a proxy that sets those headers itself.

HttpCalls Configuration

In this article we will talk about EDDI's httpCalls feature (calling other JSON APIs).

The httpCalls feature allows a Agent to consume 3rd party APIs and use the JSON response in another httpCall (for authentication or requesting a token for instance) or directly print the results in Agent's Output, this means, for example, you can call a weather API and use the JSON response in your Agent's output if the user asks about today's weather or the week's forecast!

We will emphasize the httpCall model and go through an example step by step, you can also download the example in Postman collection format and run the steps.

Model and API endpoint

{
  "targetServerUrl": "string",
  "httpCalls": [
    {
      "name": "string",
      "saveResponse": boolean,
      "fireAndForget": boolean,
      "responseObjectName": "string",
      "actions": [
        "string"
      ],
      "preRequest": {
        "batchRequests": {
          "pathToTargetArray": "string",
          "iterationObjectName": "string"
        }
      },
      "request": {
        "path": "string",
        "headers": {},
        "queryParams": {},
        "method": "string",
        "contentType": "string",
        "body": "string"
      },
      "postResponse": {
        "qrBuildInstructions": [
          {
            "pathToTargetArray": "String",
            "iterationObjectName": "String",
            "quickReplyValue": "String",
            "quickReplyExpressions": "String"
          }
        ],
        "propertyInstructions": [
          {
            "name": "string",
            "valueString": "string",
            "scope": "string",
            "fromObjectPath": "savedObjName.something.something",
            "override": boolean,
            "httpCodeValidator": {
              "runOnHttpCode": [
                <array of Integers>
              ],
              "skipOnHttpCode": [
                <array of Integers>
              ]
            }
          }
        ]
      }
    }
  ]
}

Description

An httpCall is mainly composed from the targetServer array of httpCalls, the latter will have request where you put all details about your actual http request (method,path,headers, etc..) and postResponse where you can define what happens after the httpCall has been executed and a response has been received; such as quick replies by using qrBuildInstruction.

You can use {memory.current.httpCalls.<responseObjectName>} to access your JSON object, so you can use it in output templating or in another httpCall, for example an httpCall will get the oAuth token and another httpCall will use in the http headers to authenticate to an API.

Description of the model

Element Description
targetServerUrl (String) root/context path of the httpCall (e.g http://example.com/api)
httpCall.saveResponse (Boolean) whether to save the JSON response into {memory.current.httpCalls}
httpCall.fireAndForget (Boolean) whether to execute the request without waiting for a response to be returned, (useful for POST)
httpCall.responseObjectName (String) name of the JSON object so it can be accessed from other httpCalls or outputsets.
httpCall.responseHeaderObjectName (String) name under which the RESPONSE headers are stored, reachable as {memory.current.httpCalls.<responseHeaderObjectName>.<Header-Name>} and, for an LLM tool, returned under the result's headers key. Unset by default — set it only when the answer you need is in a header (a 201's Location, say) rather than the body, since headers reach conversation memory unredacted. Header names are matched case-insensitively, and credential-bearing headers (Set-Cookie, WWW-Authenticate, …) are never stored.
httpCall.actions (String) name of the output/behavior set mapped to this http call.
httpCall.preRequest.batchRequests.pathToTargetArray (String) JSON path to the target array to be used as body of requests e.g: "memory.current.output". Only honoured when fireAndForget is true; on a call that waits for a response the batch instruction is ignored and exactly one request is sent.
httpCall.preRequest.batchRequests.iterationObjectName (String) name of the variable to be used for each element of array found in pathToTargetArray
httpCall.preRequest.batchRequests.maxBatchSize (Integer, default 100, at most 1000 — both operator-configurable, see eddi.httpcalls.batch.* in the configuration reference) most requests the batch may expand into. A target array with more elements refuses the whole call — nothing is sent and the turn fails with a message naming the limit — instead of silently dropping the tail. Saving a value above the ceiling is refused with 400.
httpCall.request.path (String) path in the targetServer of the httpCall (e.g /books)
httpCall.request.headers (Array:<key, value> ) for each httpCall HTTP header
httpCall.request.queryParams (Array: <key, value>) for each httpCall query parameter
httpCall.request.method (String) HTTP Method of the httpCall (e.g GET,POST,etc...)
httpCall.request.contentType (String) value of the contentType HTTP header of the httpCall
httpCall.request.body (String) an escaped JSON object that goes in the HTTP Request body if needed.
httpCall.postResponse.qrBuildInstructions[].pathToTargetArray (String) path to the array in your JSON response data.
httpCall.postResponse.qrBuildInstructions[].iterationObjectName (String) a variable name that will point to the TargetArray.
httpCall.postResponse.qrBuildInstructions[].quickReplyValue (String) Qute expression to use as a quickReply value.
httpCall.postResponse.qrBuildInstructions[].quickReplyExpressions (String) expression to retrieve a property from iterationObjectName.
httpCall.postResponse.propertyInstructions.name (String) name of property to be used in templating
httpCall.postResponse.propertyInstructions.valueString (String) a static value can be set here if fromObjectPath is not defined. Typed siblings exist for other value types: valueInt, valueFloat, valueBoolean, valueObject, valueList.
httpCall.postResponse.propertyInstructions.scope

(String) Can be either :

step used for only for one user interaction

conversation for entire conversation and

longTerm for between conversations

httpCall.postResponse.propertyInstructions.fromObjectPath (String) JSON path to the saved object e.g savedObjName.something.something. The value found is response data and is never rendered as a template (only valueString is). A string is stored as found — or, with convertToObject and a value shaped like a JSON object ({...}), parsed into an object. Any other value (number, boolean, JSON object or array) is stored as an empty string
httpCall.postResponse.propertyInstructions.override (Boolean) flag for override
httpCall.postResponse.propertyInstructions.httpCodeValidator.runOnHttpCode (Array: <Integer> ) a list of http code that enables this property instruction e.g [200]
httpCall.postResponse.propertyInstructions.httpCodeValidator.skipOnHttpCode (Array: <Integer>) list of http code that enables this property instruction e.g [500,501,400]

HttpCall API endpoints

HTTP Method API Endpoint Request Body Response
POST /apicallstore/apicalls http-call-model N/A
GET /apicallstore/apicalls/descriptors N/A list of references to http-call-model
DELETE /apicallstore/apicalls/{id} N/A N/A
GET /apicallstore/apicalls/{id} N/A http-call-model
PUT /apicallstore/apicalls/{id} http-call-model N/A
GET /apicallstore/apicalls/{id}/currentversion N/A current version number (text/plain integer)
POST /apicallstore/apicalls/{id}/currentversion N/A 303 redirect to /apicallstore/apicalls/{id}?version=<latest>

httpCall Sample

{
  "targetServerUrl": "https://api.agent-metrics.com/v1/messages",
  "httpCalls": [
    {
      "name": "sendUserMessageToAnalytics",
      "actions": [
        "send_input_to_analytics"
      ],
      "saveResponse": false,
      "fireAndForget": true,
      "request": {
        "method": "post",
        "queryParams": {
          "token": "<token>"
        },
        "contentType": "application/json",
        "body": "{\"text\": \"{memory.current.input}\",\"message_type\": \"incoming\",\"user_id\": \"{memory.current.userInfo.userId}\",\"platform\": \"eddi\"}"
      }
    },
    {
      "name": "sendAgentMessageToAnalytics",
      "actions": [
        "send_output_to_analytics"
      ],
      "saveResponse": false,
      "fireAndForget": true,
      "preRequest": {
        "batchRequests": {
          "pathToTargetArray": "memory.current.output",
          "iterationObjectName": "output"
        }
      },
      "request": {
        "method": "post",
        "queryParams": {
          "token": "<token>"
        },
        "contentType": "application/json",
        "body": "{\"text\": \"{output}\",\"message_type\": \"outgoing\",\"user_id\": \"{memory.current.userInfo.userId}\",\"platform\": \"eddi\"}"
      },
      "postResponse": {
        "propertyInstructions": [
          {
            "name": "nameOfPropertyToBeUsedInTemplating",
            "valueString": "StaticValueHereIfFromObjectPathIsNotDefined",
            "scope": "step",
            "fromObjectPath": "savedObjName.something.something",
            "override": true,
            "httpCodeValidator": {
              "runOnHttpCode": [
                200
              ],
              "skipOnHttpCode": [
                0,
                400,
                401,
                402,
                403,
                404,
                409,
                410,
                500,
                501,
                502
              ]
            }
          }
        ],
        "qrBuildInstructions": [
          {
            "pathToTargetArray": "savedObjName.data.topics",
            "iterationObjectName": "topic",
            "templateFilterExpression": "topic.subType != 'specialSubType'",
            "quickReplyValue": "{topic.name}",
            "quickReplyExpressions": "property(topic_id({topic.id}))"
          }
        ]
      }
    }
  ]
}

Step by step example

We will do a step by step example from scratch (Agent creation to a simple conversation that uses httpCall)

For the sake of simplicity we will use a free weather API to fetch weather of cities by their names (api.openweathermap.org).

1 - Create regularDictionnary

More about regular dictionaries can be found here.

Request URL

POST http://localhost:7070/dictionarystore/dictionaries

Request Body

{
  "words": [
    {
      "word": "weather",
      "expressions": "trigger(current_weather)",
      "frequency": 0
    }
  ],
  "phrases": [
    {
      "phrase": "what is the weather",
      "expressions": "trigger(current_weather)"
    },
    {
      "phrase": "whats the weather",
      "expressions": "trigger(current_weather)"
    }
  ]
}

Response Body

no content

Response Code

201

The Location header contains the resource URI, e.g. eddi://ai.labs.dictionary/dictionarystore/dictionaries/<id>?version=1

2 - Create the behaviorSet

More about behaviorSets can be found in Behavior Rules

Request URL

POST http://localhost:7070/rulestore/rulesets

Response Body

no content

Response Code

201

Request Body

{
  "behaviorGroups": [
    {
      "name": "",
      "behaviorRules": [
        {
          "name": "Ask for City",
          "actions": [
            "ask_for_city"
          ],
          "conditions": [
            {
              "type": "inputmatcher",
              "configs": {
                "expressions": "trigger(current_weather)"
              }
            }
          ]
        },
        {
          "name": "Current Weather in City",
          "actions": [
            "current_weather_in_city"
          ],
          "conditions": [
            {
              "type": "inputmatcher",
              "configs": {
                "expressions": "trigger(current_weather)",
                "occurrence": "lastStep"
              }
            }
          ]
        }
      ]
    }
  ]
}

The Location header contains the resource URI, e.g. eddi://ai.labs.rules/rulestore/rulesets/<id>?version=1

3 - Create the httpCall

Note that we can pass user input to the http call using {memory.current.input}

Request URL

POST http://localhost:7070/apicallstore/apicalls

Request Body

{
  "targetServerUrl": "https://api.openweathermap.org/data/2.5/weather",
  "httpCalls": [
    {
      "name": "currentWeather",
      "saveResponse": true,
      "responseObjectName": "currentWeather",
      "actions": [
        "current_weather_in_city"
      ],
      "request": {
        "path": "",
        "headers": {},
        "queryParams": {
          "APPID": "c3366d78c7c0f76d63eb4cdf1384ddbf",
          "units": "metric",
          "q": "{memory.current.input}"
        },
        "method": "get",
        "contentType": "",
        "body": ""
      }
    }
  ]
}

Response Body

no content

Response Code

201

The Location header contains the resource URI, e.g. eddi://ai.labs.apicalls/apicallstore/apicalls/<id>?version=1

4 - Create the outputSet

More about outputSet can be found Output Configuration.

Note When you set "saveResponse" : true in httpCall then you can use {memory.current.httpCalls.<responseObjectName>} to access the response data and use Qute ( {#for} ) to iterate over JSON arrays if you have them in your JSON response.

Request URL

POST http://localhost:7070/outputstore/outputsets

Request Body

{
  "outputSet": [
    {
      "action": "ask_for_city",
      "timesOccurred": 0,
      "outputs": [
        {
          "valueAlternatives": [
            {
              "type": "text",
              "text": "Which City would you like to know the weather of?"
            }
          ]
        }
      ]
    },
    {
      "action": "current_weather_in_city",
      "timesOccurred": 0,
      "outputs": [
        {
          "valueAlternatives": [
            {
              "type": "text",
              "text": "The current weather situation of {memory.current.input} is {memory.current.httpCalls.currentWeather.weather[0].description} at {memory.current.httpCalls.currentWeather.main.temp} °C"
            }
          ]
        }
      ]
    }
  ]
}

Response Body

no content

Response Code

201

The Location header contains the resource URI, e.g. eddi://ai.labs.output/outputstore/outputsets/<id>?version=1

5 - Creating the package

More about packages can be found here.

Important Workflow note

  • ai.labs.httpcalls & ai.labs.output must come after ai.labs.behavior in order of the package definition
  • ai.labs.templating has to be after ai.labs.output

Request URL

POST http://localhost:7070/workflowstore/workflows

Request Body

{
  "workflowSteps": [
    {
      "type": "eddi://ai.labs.parser",
      "extensions": {
        "dictionaries": [
          {
            "type": "eddi://ai.labs.parser.dictionaries.integer"
          },
          {
            "type": "eddi://ai.labs.parser.dictionaries.decimal"
          },
          {
            "type": "eddi://ai.labs.parser.dictionaries.punctuation"
          },
          {
            "type": "eddi://ai.labs.parser.dictionaries.email"
          },
          {
            "type": "eddi://ai.labs.parser.dictionaries.time"
          },
          {
            "type": "eddi://ai.labs.parser.dictionaries.ordinalNumber"
          },
          {
            "type": "eddi://ai.labs.parser.dictionaries.regular",
            "config": {
              "uri": "eddi://ai.labs.dictionary/dictionarystore/dictionaries/{{dictionary_id}}?version=1"
            }
          }
        ],
        "corrections": [
          {
            "type": "eddi://ai.labs.parser.corrections.stemming",
            "config": {
              "language": "english",
              "lookupIfKnown": "false"
            }
          },
          {
            "type": "eddi://ai.labs.parser.corrections.levenshtein",
            "config": {
              "distance": "2"
            }
          },
          {
            "type": "eddi://ai.labs.parser.corrections.mergedTerms"
          }
        ]
      },
      "config": {}
    },
    {
      "type": "eddi://ai.labs.behavior",
      "config": {
        "uri": "eddi://ai.labs.rules/rulestore/rulesets/{{behaviourset_id}}?version=1"
      }
    },
    {
      "type": "eddi://ai.labs.httpcalls",
      "config": {
        "uri": "eddi://ai.labs.apicalls/apicallstore/apicalls/{{httpcall_id}}?version=1"
      }
    },
    {
      "type": "eddi://ai.labs.output",
      "config": {
        "uri": "eddi://ai.labs.output/outputstore/outputsets/{{outputset_id}}?version=1"
      }
    },
    {
      "type": "eddi://ai.labs.templating",
      "extensions": {},
      "config": {}
    }
  ]
}

Response Body

no content

Response Code

201

The Location header contains the resource URI, e.g. eddi://ai.labs.workflow/workflowstore/workflows/<id>?version=1

6 - Creating the agent

Request URL

POST http://localhost:7070/agentstore/agents

Request Body

{
  "workflows": [
    "eddi://ai.labs.workflow/workflowstore/workflows/{{package_id}}?version=1"
  ],
  "channels": []
}

Response Body

no content

Response Code

201

The Location header contains the resource URI, e.g. eddi://ai.labs.agent/agentstore/agents/<id>?version=1

7 - Deploy the agent

Request URL

POST http://localhost:7070/administration/production/deploy/**<agent_id>**?version=1&autoDeploy=true

Response Body

no content

Response Code

202

8 - Create the conversation

Request URL

POST http://localhost:7070/agents/**<agent_id>**/start?environment=**<env>**

Response Body

no content

Response Code

201

The Location header contains the conversation URI.

9 - Say weather

Request URL

POST http://localhost:7070/agents/<conversation_id>?returnDetailed=false&returnCurrentStepOnly=true

Request Body

{
  "input": "weather"
}

Response Body

{
  "agentId": "5af8b075ba31c023bcb9ef3b",
  "agentVersion": 1,
  "environment": "production",
  "conversationState": "READY",
  "redoCacheSize": 0,
  "conversationSteps": [
    {
      "conversationStep": [
        {
          "key": "input:initial",
          "value": "weather"
        },
        {
          "key": "actions",
          "value": [
            "ask_for_city"
          ]
        },
        {
          "key": "output:text:ask_for_city",
          "value": "Which City would you like to know the weather of?"
        }
      ],
      "timestamp": 1526247548410
    }
  ]
}

Response Code

200

10 - Say "Vienna"

Request URL

POST http://localhost:7070/agents/<conversation_id>?returnDetailed=false&returnCurrentStepOnly=true

Request Body

{
  "agentId": "5af8b075ba31c023bcb9ef3b",
  "agentVersion": 1,
  "environment": "production",
  "conversationState": "READY",
  "redoCacheSize": 0,
  "conversationSteps": [
    {
      "conversationStep": [
        {
          "key": "input:initial",
          "value": "Vienna"
        },
        {
          "key": "actions",
          "value": [
            "current_weather_in_city"
          ]
        },
        {
          "key": "output:text:current_weather_in_city",
          "value": "The current weather situation of Vienna is clear sky at 17.68 °C"
        }
      ],
      "timestamp": 1526247618080
    }
  ]
}

Response Code

200

Full Example

Run it yourself. The GitBook-hosted Postman collections that used to be linked here were lost in the migration. You do not need them: every request is shown inline above, and Postman can import EDDI's own spec directly — Import → Link → <your-eddi-host>/openapi (http://localhost:7070/openapi for a local install). It is generated from the running build, so unlike a committed collection it cannot go out of date. The same spec is browsable at <your-eddi-host>/q/swagger-ui.