Skip to content

Commit a39b49c

Browse files
authored
[api][runtime] Support Short-Term Memory (#67)
1 parent b55ca1c commit a39b49c

18 files changed

Lines changed: 1265 additions & 12 deletions

File tree

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.flink.agents.api.context;
19+
20+
import java.util.List;
21+
import java.util.Map;
22+
23+
/**
24+
* A representation of an object in the short-term memory. It is responsible for accessing and
25+
* manipulating (direct or indirect) fields within the memory structure. A direct field is a field
26+
* which stores primitive data directly, while an indirect filed is a field which represents a
27+
* nested object.Fields can be accessed using an absolute or relative path.
28+
*/
29+
public interface MemoryObject {
30+
/**
31+
* Returns a MemoryObject that represents the given path.
32+
*
33+
* @param path relative path from the current object to the target field
34+
* @return a MemoryObject instance pointing to the field. If the field is a primitive value
35+
* type, the value of the returned MemoryObject can be exposed via {@link #getValue()}.If
36+
* the field is a nested object, the subfields of the returned MemoryObject can be exposed
37+
* via {@link #getFields()}.
38+
* @throws Exception if the field does not exist
39+
*/
40+
MemoryObject get(String path) throws Exception;
41+
42+
/**
43+
* Sets the value of a direct field in the current object; any missing intermediate objects will
44+
* be created automatically.
45+
*
46+
* @param path relative path from the current object to the target field
47+
* @param value new value of the field
48+
* @throws Exception if trying to overwrite a nested object with a primitive value or set a
49+
* MemoryObject directly
50+
*/
51+
void set(String path, Object value) throws Exception;
52+
53+
/**
54+
* Creates a new MemoryObject as an indirect field in the current object.
55+
*
56+
* @param path relative path from the current object to the target field
57+
* @param overwrite whether to overwrite existing field if it's not a nested object
58+
* @return the created object
59+
* @throws Exception if field exists but is not a nested object and overwrite is false
60+
*/
61+
MemoryObject newObject(String path, boolean overwrite) throws Exception;
62+
63+
/**
64+
* Checks whether a (direct or indirect) field exists in the current object.
65+
*
66+
* @param path relative path from the current object to the target field
67+
* @return true if the field exists, false otherwise
68+
*/
69+
boolean isExist(String path);
70+
71+
/**
72+
* Gets names of all the top-level fields of the current object.
73+
*
74+
* @return list of top-level field names
75+
* @throws Exception state-backend failure
76+
*/
77+
List<String> getFieldNames() throws Exception;
78+
79+
/**
80+
* Gets all the top-level fields of the current object.
81+
*
82+
* @return map of top-level fields
83+
* @throws Exception state-backend failure
84+
*/
85+
Map<String, Object> getFields() throws Exception;
86+
87+
/**
88+
* Gets the primitive value stored at the current path.
89+
*
90+
* @return the primitive value if this MemoryObject stores primitive value directly, or null if
91+
* it represents a nested object
92+
* @throws Exception state-backend failure
93+
*/
94+
Object getValue() throws Exception;
95+
96+
/**
97+
* Checks whether the current object is a nested object.
98+
*
99+
* @return true if this MemoryObject is a nested object, false if it stores primitive value
100+
* directly
101+
* @throws Exception state-backend failure
102+
*/
103+
boolean isNestedObject() throws Exception;
104+
}

api/src/main/java/org/apache/flink/agents/api/context/RunnerContext.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,12 @@ public interface RunnerContext {
3030
* @param event the event to be sent
3131
*/
3232
void sendEvent(Event event);
33+
34+
/**
35+
* Gets the short-term memory.
36+
*
37+
* @return MemoryObject the root of the short-term memory
38+
* @throws Exception if the underlying state backend cannot be accessed
39+
*/
40+
MemoryObject getShortTermMemory() throws Exception;
3341
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
################################################################################
2+
# Licensed to the Apache Software Foundation (ASF) under one
3+
# or more contributor license agreements. See the NOTICE file
4+
# distributed with this work for additional information
5+
# regarding copyright ownership. The ASF licenses this file
6+
# to you under the Apache License, Version 2.0 (the
7+
# "License"); you may not use this file except in compliance
8+
# with the License. You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
#################################################################################
18+
from abc import ABC, abstractmethod
19+
from typing import Any, Dict, List
20+
21+
from pydantic import BaseModel
22+
23+
24+
class MemoryObject(BaseModel, ABC):
25+
"""Representation of an object in the short-term memory.
26+
27+
A direct field is a field which stores concrete data directly, while an indirect
28+
filed is just a field which represents a nested object. Fields can be accessed
29+
using an absolute or relative path.
30+
"""
31+
32+
@abstractmethod
33+
def get(self, path: str) -> Any:
34+
"""Get the value of a (direct or indirect) field in the object.
35+
36+
Parameters
37+
----------
38+
path: str
39+
Relative path from the current object to the target field.
40+
41+
Returns:
42+
-------
43+
Any
44+
If the field is a direct field, returns the concrete data stored.
45+
If the field is an indirect field, another MemoryObject will be returned.
46+
If the field doesn't exist, returns None.
47+
"""
48+
49+
@abstractmethod
50+
def set(self, path: str, value: Any) -> None:
51+
"""Set the value of an indirect field in the object.
52+
This will also create the intermediate objects if not exist.
53+
54+
Parameters
55+
----------
56+
path: str
57+
Relative path from the current object to the target field.
58+
value: Any
59+
New value of the field. The type of the value must be a primary type.
60+
"""
61+
62+
@abstractmethod
63+
def new_object(self, path: str) -> "MemoryObject":
64+
"""Create a new object as the value of an indirect field in the object.
65+
66+
Parameters
67+
----------
68+
path: str
69+
Relative path from the current object to the target field.
70+
71+
Returns:
72+
-------
73+
MemoryObject
74+
The created object.
75+
"""
76+
77+
@abstractmethod
78+
def is_exist(self, path: str) -> bool:
79+
"""Check whether a (direct or indirect) field exist in the object.
80+
81+
Parameters
82+
----------
83+
path: str
84+
Relative path from the current object to the target field.
85+
86+
Returns:
87+
-------
88+
bool
89+
Whether the field exists.
90+
"""
91+
92+
@abstractmethod
93+
def get_field_names(self) -> List[str]:
94+
"""Get names of all the top-level subfields of the object.
95+
96+
Returns:
97+
-------
98+
List[str]
99+
Top-level subfield names of the object in a list.
100+
"""
101+
102+
@abstractmethod
103+
def get_fields(self) -> Dict[str, Any]:
104+
"""Get all the top-level subfields of the object.
105+
106+
Returns:
107+
-------
108+
Dict[str, Any]
109+
Top-level subfields of the object in a dictionary.
110+
"""

python/flink_agents/api/runner_context.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from abc import ABC, abstractmethod
1919

2020
from flink_agents.api.event import Event
21+
from flink_agents.api.memory_object import MemoryObject
2122
from flink_agents.api.resource import Resource, ResourceType
2223

2324

@@ -48,3 +49,13 @@ def get_resource(self, name: str, type: ResourceType) -> Resource:
4849
type : ResourceType
4950
The type of the resource.
5051
"""
52+
53+
@abstractmethod
54+
def get_short_term_memory(self) -> MemoryObject:
55+
"""Get the short-term memory.
56+
57+
Returns:
58+
-------
59+
MemoryObject
60+
The root object of the short-term memory.
61+
"""

python/flink_agents/examples/agent_example.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,32 @@ class MyAgent(Agent):
4040
@staticmethod
4141
def first_action(event: Event, ctx: RunnerContext): # noqa D102
4242
input = event.input
43-
content = input + " first_action"
43+
memory = ctx.get_short_term_memory()
44+
45+
current_count = memory.get("action_counter") or 0
46+
new_count = current_count + 1
47+
memory.set("action_counter", new_count)
48+
49+
content = input + " -> first_action"
50+
key_with_count = f"(seen {new_count} times)"
51+
4452
ctx.send_event(MyEvent(value=content))
45-
ctx.send_event(OutputEvent(output=content))
53+
ctx.send_event(OutputEvent(output={key_with_count: content}))
4654

4755
@action(MyEvent)
4856
@staticmethod
4957
def second_action(event: Event, ctx: RunnerContext): # noqa D102
5058
input = event.value
51-
content = input + " second_action"
52-
ctx.send_event(OutputEvent(output=content))
59+
memory = ctx.get_short_term_memory()
60+
61+
current_count = memory.get("action_counter")
62+
new_count = current_count + 1
63+
memory.set("action_counter", new_count)
64+
65+
base_message = input.split("->")[0].strip()
66+
content = base_message + " -> second_action"
67+
key_with_count = f"(seen {new_count} times)"
68+
ctx.send_event(OutputEvent(output={key_with_count: content}))
5369

5470

5571
if __name__ == "__main__":
@@ -62,8 +78,10 @@ def second_action(event: Event, ctx: RunnerContext): # noqa D102
6278

6379
input_list.append({"key": "bob", "value": "The message from bob"})
6480
input_list.append({"k": "john", "v": "The message from john"})
81+
input_list.append({"key": "john", "value": "Second message from john"})
82+
input_list.append({"key": "bob", "value": "Second message from bob"})
6583
input_list.append(
66-
{"value": "The message from unknown"}
84+
{"value": "Message from unknown"}
6785
) # will automatically generate a new unique key
6886

6987
env.execute()

python/flink_agents/examples/my_agent.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
# limitations under the License.
1717
#################################################################################
1818
import copy
19-
from typing import Any
19+
from typing import Any, Optional
2020

2121
from pydantic import BaseModel
2222

@@ -42,6 +42,7 @@ class ItemData(BaseModel):
4242
id: int
4343
review: str
4444
review_score: float
45+
memory_info: Optional[dict] = None
4546

4647

4748
class MyEvent(Event): # noqa D101
@@ -60,6 +61,16 @@ class DataStreamAgent(Agent):
6061
@staticmethod
6162
def first_action(event: Event, ctx: RunnerContext): # noqa D102
6263
input = event.input
64+
65+
stm = ctx.get_short_term_memory()
66+
status = stm.new_object("status", overwrite = True)
67+
68+
total = 0
69+
if stm.is_exist("status.total_reviews"):
70+
total = status.get("total_reviews")
71+
total += 1
72+
status.set("total_reviews", total)
73+
6374
content = copy.deepcopy(input)
6475
content.review += " first action"
6576
ctx.send_event(MyEvent(value=content))
@@ -68,8 +79,15 @@ def first_action(event: Event, ctx: RunnerContext): # noqa D102
6879
@staticmethod
6980
def second_action(event: Event, ctx: RunnerContext): # noqa D102
7081
input = event.value
82+
83+
stm = ctx.get_short_term_memory()
84+
memory_info = {
85+
"total_reviews": stm.get("status.total_reviews"),
86+
}
87+
7188
content = copy.deepcopy(input)
7289
content.review += " second action"
90+
content.memory_info = memory_info
7391
ctx.send_event(OutputEvent(output=content))
7492

7593

0 commit comments

Comments
 (0)