Skip to content

Commit f7c6bb1

Browse files
committed
feat: add ability to imperatively defining the mapping between input ouput
1 parent 0b79b0a commit f7c6bb1

14 files changed

+761
-34
lines changed

Diff for: README.md

+9-6
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,15 @@ Communication between agents is not possible if there are discrepancies between
1515

1616
Ensuring that agents are semantically compatible, i.e., the output of the one agent contains the information needed
1717
by later agents, is an problem of composition or planning in the application. This project, the IO Mapper Agent,
18-
addresses level 2 and 3 compatibility. It is a component, implemented as an agent, that can make use of an LLM
18+
addresses level 2 and 3 compatibility. It is a component, implemented as an agent, that can make use of an LLM
1919
to transform the output of one agent to become compatible to the input of another agent. Note that this may mean
2020
many different things, for example:
2121

22-
* JSON structure transcoding: A JSON dictionary needs to be remapped into another JSON dictionary
23-
* Text summarisation: A text needs to be summarised or some information needs to be removed
24-
* Text translation: A text needs to be translated from one language to another
25-
* Text manipulation: Part of the information of one text needs to be reformulated into another text
26-
* Any combination of the above
22+
- JSON structure transcoding: A JSON dictionary needs to be remapped into another JSON dictionary
23+
- Text summarisation: A text needs to be summarised or some information needs to be removed
24+
- Text translation: A text needs to be translated from one language to another
25+
- Text manipulation: Part of the information of one text needs to be reformulated into another text
26+
- Any combination of the above
2727

2828
The IO mapper Agent can be fed the schema definitions of inputs and outputs as defined by the [Agent Connect Protocol](https://github.com/agntcy/acp-spec).
2929

@@ -43,6 +43,9 @@ To get a local copy up and running follow these simple steps.
4343

4444
## Usage
4545

46+
Learn how to use our different Mappers
47+
[USAGE.md](_usage.md)
48+
4649
## Contributing
4750

4851
Contributions are what make the open source community such an amazing place to

Diff for: agntcy_iomapper/__init__.py

+4
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,7 @@
1212
IOMapperConfig,
1313
IOModelArgs,
1414
)
15+
16+
from .imperative import ImperativeIOMapper
17+
18+
___all__ = [ "ImperativeIOMapper", "AgentIOMapper", "IOMapperOutput", "IOMapperInput"]

Diff for: agntcy_iomapper/base.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
# SPDX-FileCopyrightText: Copyright (c) 2025 Cisco and/or its affiliates.
22
# SPDX-License-Identifier: Apache-2.0
3-
from abc import abstractmethod, ABC
4-
from pydantic import BaseModel, model_validator, Field
5-
from typing import Any
3+
from abc import ABC, abstractmethod
4+
from typing import Any, TypedDict
5+
66
from openapi_pydantic import Schema
7+
from pydantic import BaseModel, Field, model_validator
78
from typing_extensions import Self
8-
from typing import TypedDict
99

1010

1111
class ArgumentsDescription(BaseModel):

Diff for: agntcy_iomapper/imperative.py

+136
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025 Cisco and/or its affiliates.
2+
# SPDX-License-Identifier: Apache-2.0"
3+
"""
4+
The deterministic I/O mapper is a component
5+
designed to translate specific inputs into
6+
corresponding outputs in a predictable and consistent manner.
7+
When configured with a JSONPath definition,
8+
this mapper utilizes the JSONPath query language
9+
to extract data from JSON-formatted input,
10+
transforming it into a structured output based on predefined rules.
11+
The deterministic nature of the mapper ensures that given the same input and
12+
JSONPath configuration, the output will always be the same,
13+
providing reliability and repeatability.
14+
This is particularly useful in scenarios where
15+
consistent data transformation is required.
16+
"""
17+
18+
import json
19+
import logging
20+
from typing import Any, Callable, Dict, Union
21+
22+
import jsonschema
23+
from jsonpath_ng.ext import parse
24+
25+
from agntcy_iomapper.base import BaseIOMapper, IOMapperInput, IOMapperOutput
26+
27+
logger = logging.getLogger(__name__)
28+
29+
30+
class ImperativeIOMapper(BaseIOMapper):
31+
field_mapping: Dict[str, Union[str, Callable]]
32+
"""A dictionary for where the keys are fields of the output object
33+
and values are JSONPath (strings) representing how the mapping
34+
"""
35+
36+
def __init__(self, field_mapping: Dict[str, Union[str, Callable]]) -> None:
37+
super().__init__()
38+
self.field_mapping = field_mapping
39+
40+
def invoke(self, input: IOMapperInput) -> IOMapperOutput | None:
41+
if input.data is None:
42+
return None
43+
if self.field_mapping is None:
44+
return IOMapperOutput(data=input.data)
45+
46+
data = self._imperative_map(input)
47+
return IOMapperOutput(data=data)
48+
49+
def ainvoke(self, input: IOMapperInput) -> IOMapperOutput | None:
50+
return self.invoke(input)
51+
52+
def _imperative_map(self, input_definition: IOMapperInput) -> Any:
53+
"""
54+
Converts input data to a desired output type.
55+
56+
This function attempts to convert the provided data into the specified
57+
target type. It performs validation using a JSON schema and raises a
58+
ValidationError if the data does not conform to the expected schema for
59+
the target type.
60+
61+
Parameters:
62+
----------
63+
data : Any
64+
The input data to be converted. This can be of any type.
65+
Returns:
66+
-------
67+
Any
68+
The converted data in the desired output type.
69+
Raises:
70+
------
71+
ValidationError
72+
If the input data does not conform to the expected schema for the
73+
target type.
74+
Notes:
75+
-----
76+
The function assumes that the caller provides a valid `input_schema`.
77+
Unsupported target types should be handled as needed within the function.
78+
"""
79+
data = input_definition.data
80+
input_schema = input_definition.input.json_schema
81+
82+
jsonschema.validate(
83+
instance=data,
84+
schema=input_schema.model_dump(exclude_none=True, mode="json"),
85+
)
86+
87+
mapped_output = {}
88+
89+
for output_field, json_path_or_func in self.field_mapping.items():
90+
if isinstance(json_path_or_func, str):
91+
jsonpath_expr = parse(json_path_or_func)
92+
match = jsonpath_expr.find(data)
93+
expect_value = match[0].value if match else None
94+
elif callable(json_path_or_func):
95+
expect_value = json_path_or_func(data)
96+
else:
97+
raise TypeError(
98+
"Mapping values must be strings (JSONPath) or callables (functions)."
99+
)
100+
101+
self._set_jsonpath(mapped_output, output_field, expect_value)
102+
jsonschema.validate(
103+
instance=mapped_output,
104+
schema=input_definition.output.json_schema.model_dump(
105+
exclude_none=True, mode="json"
106+
),
107+
)
108+
# return a serialized version of the object
109+
return json.dumps(mapped_output)
110+
111+
def _set_jsonpath(
112+
self, data: dict[str, Any], path: str, value: Any
113+
) -> dict[str, Any]:
114+
"""set value for field based on its json path
115+
Args:
116+
data: Data so far
117+
path: the json path
118+
value: the value to set the json path to
119+
Returns:
120+
-----
121+
dict[str,Any]
122+
The mapped filed with the value
123+
"""
124+
copy_data: dict[str, Any] = data
125+
# Split the path into parts and remove the leading root
126+
parts = path.strip("$.").split(".")
127+
# Add value to corresponding path
128+
for part in parts[:-1]:
129+
if part not in copy_data:
130+
copy_data[part] = {}
131+
132+
copy_data = copy_data[part]
133+
134+
copy_data[parts[-1]] = value
135+
136+
return copy_data

Diff for: docs/README.md

+10-7
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@ Communication between agents is not possible if there are discrepancies between
1212

1313
Ensuring that agents are semantically compatible, i.e., the output of the one agent contains the information needed
1414
by later agents, is an problem of composition or planning in the application. This project, the IO Mapper Agent,
15-
addresses level 2 and 3 compatibility. It is a component, implemented as an agent, that can make use of an LLM
15+
addresses level 2 and 3 compatibility. It is a component, implemented as an agent, that can make use of an LLM
1616
to transform the output of one agent to become compatible to the input of another agent. Note that this may mean
1717
many different things, for example:
1818

19-
* JSON structure transcoding: A JSON dictionary needs to be remapped into another JSON dictionary
20-
* Text summarisation: A text needs to be summarised or some information needs to be removed
21-
* Text translation: A text needs to be translated from one language to another
22-
* Text manipulation: Part of the information of one text needs to be reformulated into another text
23-
* Any combination of the above
19+
- JSON structure transcoding: A JSON dictionary needs to be remapped into another JSON dictionary
20+
- Text summarisation: A text needs to be summarised or some information needs to be removed
21+
- Text translation: A text needs to be translated from one language to another
22+
- Text manipulation: Part of the information of one text needs to be reformulated into another text
23+
- Any combination of the above
2424

2525
The IO mapper Agent can be fed the schema definitions of inputs and outputs as defined by the [Agent Connect Protocol](https://github.com/agntcy/acp-spec).
2626

@@ -40,6 +40,9 @@ To get a local copy up and running follow these simple steps.
4040

4141
## Usage
4242

43+
Learn how to use our different Mappers
44+
[USAGE.md](_usage.md)
45+
4346
## Contributing
4447

4548
Contributions are what make the open source community such an amazing place to
@@ -61,7 +64,7 @@ Licensed under the Apache License, Version 2.0 (the "License");
6164
you may not use this file except in compliance with the License.
6265
You may obtain a copy of the License at
6366

64-
http://www.apache.org/licenses/LICENSE-2.0
67+
http://www.apache.org/licenses/LICENSE-2.0
6568

6669
Unless required by applicable law or agreed to in writing, software
6770
distributed under the License is distributed on an "AS IS" BASIS,

Diff for: docs/_usage.md

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Usage
2+
3+
### Use Agent IO Mapper
4+
5+
:TODO
6+
7+
### Use Determenistic
8+
9+
The code snippet below illustrates a fully functional deterministic mapping that transforms the output of one agent into input for a second agent. The code for the agents is omitted.
10+
11+
```python
12+
#define schema for the origin agent
13+
input_schema = {"question": {"type": "string"}}
14+
15+
#define schema to witch the input should be converted to
16+
output_schema = {
17+
"quiz": {
18+
"type": "object",
19+
"properties": {
20+
"prof_question": {"type": "string"},
21+
"due_date": {"type": "string"},
22+
},
23+
}
24+
}
25+
26+
#the mapping object using jsonpath, note: the value of the mapping can be either a jsonpath or a function
27+
mapping_object = {
28+
"prof_question": "$.question",
29+
"due_date": lambda _: datetime.now().strftime("%x"),
30+
}
31+
32+
input = IOMapperInput(
33+
input=ArgumentsDescription(
34+
json_schema=Schema.model_validate(input_schema)
35+
),
36+
output=ArgumentsDescription(
37+
json_schema=Schema.model_validate(output_schema)
38+
),
39+
data={"question": output_prof},
40+
)
41+
#instantiate the mapper
42+
imerative_mapp = ImperativeIOMapper(
43+
field_mapping=mapping_object,
44+
)
45+
#get the mapping result and send to the other agent
46+
mapping_result = imerative_mapp.invoke(input=input)
47+
48+
49+
50+
```
51+
52+
### Use Examples
53+
54+
1. To run the examples we strongly recommend that a [virtual environment is created](https://packaging.python.org/en/latest/guides/installing-using-pip-and-virtual-environments/)
55+
2. Install the requirements file
56+
3. from within examples folder run:
57+
58+
```sh make run_imperative_example
59+
60+
[GitHub](https://github.com/agntcy/iomapper-agnt/)
61+
[Get Started](#getting-started)
62+
```

Diff for: examples/Makefile

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
2+
EXAMPLES ?= .
3+
4+
run_imperative_example:
5+
python $(EXAMPLES)/imperative.py

Diff for: examples/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)