Skip to content

Customise what agents 'see'

In Your first experiment, we put each agent's private value or cost directly in its prompt. This works fine for a single buyer and seller, but it doesn't scale well to larger markets with multiple agents. A better solution is to keep the private information in a separate list, and then write a small Python class that exposes each agent's own value or cost to it before each decision. We call this class a context provider because it provides context to the agent.

Create an experiment.yaml file

Download the complete files and extract them into your experiment folder, keeping the examples/ directory structure. Create or edit this stage's files in examples/tutorials/custom_information.

We'll keep the buyer values and seller costs in the variables section of our YAML file. Think of these lists as the market's demand and supply schedules: each entry belongs to one participant.

Create a new file called experiment.yaml and copy the contents of the snippet below into it. The highlighted lines show what has changed since the first experiment.

experiment.yaml
variables:
  buyer_values: ['100', '80', '60']
  seller_costs: ['40', '60', '90']
experiment:
  name: custom_information
  seed: 42
  output_dir: results
  protocol:
    period_duration_seconds: 30
  assets:
  - id: asset
    name: Good
  markets:
  - id: asset
    price_min: '0'
    price_max: '150'
  agents:
  - id: buyer
    n: 3
    lm:
      provider: openai
      model: gpt-5.4-mini
      temperature: null
      reasoning_effort: none
      max_tokens: 4096
    prompt:
      instructions:
        content: |
          You are a buyer. You can trade one unit per round.
          Your private value appears in each market update.
          Your earnings are your value minus the purchase price.
          If you do not trade, your earnings are zero.
          Maximise your earnings. Do not buy above your value. Do not sell.
      context_providers:
      - type: examples.tutorials.custom_information.providers.PrivateValueProvider
        params:
          buyer_values: ${variables.buyer_values}
          seller_costs: ${variables.seller_costs}
      - type: snapshot_period_timing
      - type: snapshot_order_book
      - type: snapshot_own_orders
      - type: snapshot_own_holdings
      - type: snapshot_last_action_outcome
    initial_holdings:
      cash: '150'
      units:
        asset: 0
    trading_constraints:
      allowed_sides: [bid]
      max_units:
        asset: 1
      max_order_quantity: 1
      max_outstanding_orders: 1
  - id: seller
    n: 3
    lm:
      provider: openai
      model: gpt-5.4-mini
      temperature: null
      reasoning_effort: none
      max_tokens: 4096
    prompt:
      instructions:
        content: |
          You are a seller. You can trade one unit per round.
          Your private cost appears in each market update.
          Your earnings are your sale price minus your cost.
          If you do not trade, your earnings are zero.
          Maximise your earnings. Do not sell below your cost. Do not buy.
      context_providers:
      - type: examples.tutorials.custom_information.providers.PrivateValueProvider
        params:
          buyer_values: ${variables.buyer_values}
          seller_costs: ${variables.seller_costs}
      - type: snapshot_period_timing
      - type: snapshot_order_book
      - type: snapshot_own_orders
      - type: snapshot_own_holdings
      - type: snapshot_last_action_outcome
    initial_holdings:
      cash: '0'
      units:
        asset: 1
    trading_constraints:
      allowed_sides: [ask]
      max_order_quantity: 1
      max_outstanding_orders: 1

Each group now has n: 3, so aigora creates buyer_1, buyer_2, and buyer_3, along with three sellers. The first buyer gets the first value, the second buyer gets the second value, and so on. Keep one entry in each list for each participant in that group.

We've also added PrivateValueProvider to each group's context_providers list. The ${variables.buyer_values} expression passes the buyer schedule to this provider; the seller schedule works the same way. Now we just need to write the Python class that tells each agent its own value or cost.

Keep relevant parameters in experiment.yaml

Try and keep the experiment.yaml as the single source of truth for parameters that are relevant to the experiment. A seemingly innocuous temptation might be to hard-code the values and costs in the provider class, but a suggested rule of thumb is to keep the experiment's parameters in the YAML file, and the logic for how to use them in Python.

Write a context provider

A context provider is a class with a get_context method that returns text. Before each decision, aigora adds this text to the agent's market update.

Aigora provides several built-in providers that supply information about the market, such as the current time, the orders available, and the agent's own holdings. But you can also write your own custom providers to supply any information you want, such as the agent's private value or cost.

Create a file called providers.py alongside experiment.yaml, and copy in the following code:

providers.py
import aigora


class PrivateValueProvider:
    def __init__(self, buyer_values, seller_costs):
        self.values = {}
        for number, value in enumerate(buyer_values, start=1):
            self.values[f"buyer_{number}"] = value
        for number, cost in enumerate(seller_costs, start=1):
            self.values[f"seller_{number}"] = cost

    def get_context(self, context: aigora.ProviderContext) -> str:
        agent_id = context.agent.agent_id
        label = "value" if agent_id.startswith("buyer_") else "cost"
        return f"Your private {label} for this round is {self.values[agent_id]}."

At a minimum, a context provider requires a constructor and a get_context method. Use the constructor to store any information that was passed to the provider in the YAML file, such as the buyer values or seller costs. Use get_context to compute the text that will be added to the agent's market update.

Tip: Use the ProviderContext object

The get_context method receives a ProviderContext object, which contains the all the information that aigora has about the agent and the market at that point in time. When writing a custom provider, you will often rely on this object to determine what to return. The full list of attributes is documented in the context provider reference.

In the example above, the constructor receives the list of values or costs, and then creates a dictionary that pairs each agent's ID with its entry in the schedule. The get_context method then looks up that entry and returns a short message. For buyer_2, that message is:

Your private value for this round is 80.

That's all there is to it. Our provider supplies the private information, while the built-in providers continue to supply orders, holdings, and timing. The dotted path in YAML tells aigora where to find our class.

Run the experiment

Create run.py alongside the other files:

run.py
import asyncio

import aigora

if __name__ == "__main__":
    run_dir = asyncio.run(
        aigora.run("examples/tutorials/custom_information/experiment.yaml")
    )
    print(f"Experiment results saved to {run_dir}")

Then, run the script from your experiment folder (the folder containing examples/):

python -m examples.tutorials.custom_information.run

When the run finishes, you'll find the trades in trades.csv and each agent's decisions in traces/ inside the printed results directory. Have a look at buyer_2's trace: its updates contain its own value of 80, while the other participants' values stay private. The complete schedules are saved in the run's configuration.

You now have a market with six participants, each trading on its own private information. In the next example, we'll give our agents a few more rounds to learn the ropes and see how their strategies evolve over time.

Download the complete files if you want a separate copy of this stage.