nora-lib 0.0.7.dev2__tar.gz → 0.1.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/PKG-INFO +1 -1
- {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/nora_lib/interactions/interactions_service.py +113 -10
- {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/nora_lib/interactions/models.py +14 -0
- {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/nora_lib.egg-info/PKG-INFO +1 -1
- {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/nora_lib.egg-info/SOURCES.txt +1 -0
- {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/setup.py +1 -1
- nora_lib-0.1.8/tests/test_virtual_threads.py +58 -0
- {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/README.md +0 -0
- {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/nora_lib/__init__.py +0 -0
- {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/nora_lib/context/__init__.py +0 -0
- {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/nora_lib/context/context_service.py +0 -0
- {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/nora_lib/context/models.py +0 -0
- {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/nora_lib/interactions/__init__.py +0 -0
- {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/nora_lib/py.typed +0 -0
- {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/nora_lib/tasks/__init__.py +0 -0
- {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/nora_lib/tasks/models.py +0 -0
- {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/nora_lib/tasks/state.py +0 -0
- {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/nora_lib.egg-info/dependency_links.txt +0 -0
- {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/nora_lib.egg-info/requires.txt +0 -0
- {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/nora_lib.egg-info/top_level.txt +0 -0
- {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/pyproject.toml +0 -0
- {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/setup.cfg +0 -0
- {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/tests/tasks/__init__.py +0 -0
- {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/tests/tasks/test_state.py +0 -0
- {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/tests/test_placeholder.py +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import requests
|
|
2
|
-
from typing import Optional
|
|
2
|
+
from typing import Optional, List
|
|
3
3
|
import json
|
|
4
4
|
|
|
5
5
|
from nora_lib.interactions.models import (
|
|
@@ -8,6 +8,7 @@ from nora_lib.interactions.models import (
|
|
|
8
8
|
Message,
|
|
9
9
|
ReturnedMessage,
|
|
10
10
|
ThreadRelationsResponse,
|
|
11
|
+
VirtualThread,
|
|
11
12
|
)
|
|
12
13
|
|
|
13
14
|
|
|
@@ -21,8 +22,13 @@ class InteractionsService:
|
|
|
21
22
|
self.timeout = timeout
|
|
22
23
|
self.headers = {"Authorization": f"Bearer {token}"} if token else None
|
|
23
24
|
|
|
24
|
-
def save_message(
|
|
25
|
-
|
|
25
|
+
def save_message(
|
|
26
|
+
self, message: Message, virtual_thread_id: Optional[str] = None
|
|
27
|
+
) -> None:
|
|
28
|
+
"""
|
|
29
|
+
Save a message to the Interaction Store
|
|
30
|
+
:param virtual_thread_id: Optional ID of a virtual thread to associate with the message
|
|
31
|
+
"""
|
|
26
32
|
message_url = f"{self.base_url}/interaction/v1/message"
|
|
27
33
|
response = requests.post(
|
|
28
34
|
message_url,
|
|
@@ -31,9 +37,25 @@ class InteractionsService:
|
|
|
31
37
|
timeout=self.timeout,
|
|
32
38
|
)
|
|
33
39
|
response.raise_for_status()
|
|
40
|
+
if virtual_thread_id:
|
|
41
|
+
# Use an event to tag the message with the virtual thread ID
|
|
42
|
+
event = Event(
|
|
43
|
+
type=VirtualThread.EVENT_TYPE,
|
|
44
|
+
actor_id=message.actor_id,
|
|
45
|
+
message_id=message.message_id,
|
|
46
|
+
data={
|
|
47
|
+
VirtualThread.ID_FIELD: virtual_thread_id,
|
|
48
|
+
VirtualThread.EVENT_TYPE_FIELD: VirtualThread.EVENT_TYPE,
|
|
49
|
+
},
|
|
50
|
+
timestamp=message.ts,
|
|
51
|
+
)
|
|
52
|
+
self.save_event(event)
|
|
34
53
|
|
|
35
|
-
def save_event(self, event: Event) -> None:
|
|
36
|
-
"""
|
|
54
|
+
def save_event(self, event: Event, virtual_thread_id: Optional[str] = None) -> None:
|
|
55
|
+
"""
|
|
56
|
+
Save an event to the Interaction Store
|
|
57
|
+
:param virtual_thread_id: Optional ID of a virtual thread to associate with the event
|
|
58
|
+
"""
|
|
37
59
|
event_url = f"{self.base_url}/interaction/v1/event"
|
|
38
60
|
response = requests.post(
|
|
39
61
|
event_url,
|
|
@@ -42,6 +64,77 @@ class InteractionsService:
|
|
|
42
64
|
timeout=self.timeout,
|
|
43
65
|
)
|
|
44
66
|
response.raise_for_status()
|
|
67
|
+
if virtual_thread_id:
|
|
68
|
+
# Use an event to tag the event with the virtual thread ID
|
|
69
|
+
# Attach it to the same message as this event, along with the event type
|
|
70
|
+
event = Event(
|
|
71
|
+
type=VirtualThread.EVENT_TYPE,
|
|
72
|
+
actor_id=event.actor_id,
|
|
73
|
+
message_id=event.message_id,
|
|
74
|
+
data={
|
|
75
|
+
VirtualThread.ID_FIELD: virtual_thread_id,
|
|
76
|
+
VirtualThread.EVENT_TYPE_FIELD: event.type,
|
|
77
|
+
},
|
|
78
|
+
timestamp=event.timestamp,
|
|
79
|
+
)
|
|
80
|
+
self.save_event(event)
|
|
81
|
+
|
|
82
|
+
def get_virtual_thread_content(
|
|
83
|
+
self, message_id: str, virtual_thread_id: str
|
|
84
|
+
) -> List[ReturnedMessage]:
|
|
85
|
+
"""Fetch all messages and events in a virtual thread
|
|
86
|
+
Returns all messages and events in the same thread as the given message,
|
|
87
|
+
but filtered to only include those associated with the given virtual thread.
|
|
88
|
+
:param message_id: The ID of a message in the virtual thread
|
|
89
|
+
:param virtual_thread_id: The ID of the virtual thread
|
|
90
|
+
"""
|
|
91
|
+
message_search_url = f"{self.base_url}/interaction/v1/search/message"
|
|
92
|
+
# Fetch all events and filter on the client side
|
|
93
|
+
# Need an IStore schema change to do this server-side
|
|
94
|
+
request_body = {
|
|
95
|
+
"id": message_id,
|
|
96
|
+
"relations": {
|
|
97
|
+
"preceding_messages": {
|
|
98
|
+
"max": 100,
|
|
99
|
+
"relations": {"events": {}},
|
|
100
|
+
},
|
|
101
|
+
"events": {},
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
response = requests.post(
|
|
106
|
+
message_search_url,
|
|
107
|
+
json=request_body,
|
|
108
|
+
headers=self.headers,
|
|
109
|
+
timeout=self.timeout,
|
|
110
|
+
)
|
|
111
|
+
response.raise_for_status()
|
|
112
|
+
result = ReturnedMessage.model_validate(response.json()["message"])
|
|
113
|
+
all_messages = result.preceding_messages + [result]
|
|
114
|
+
virtual_thread_content = []
|
|
115
|
+
for msg in all_messages:
|
|
116
|
+
event_types_in_virtual_thread = set(
|
|
117
|
+
event.data[VirtualThread.EVENT_TYPE_FIELD]
|
|
118
|
+
for event in msg.events
|
|
119
|
+
if event.type == VirtualThread.EVENT_TYPE
|
|
120
|
+
and event.data.get(VirtualThread.ID_FIELD) == virtual_thread_id
|
|
121
|
+
)
|
|
122
|
+
if not event_types_in_virtual_thread:
|
|
123
|
+
continue
|
|
124
|
+
virtual_thread_content.append(msg)
|
|
125
|
+
msg.events = [
|
|
126
|
+
event
|
|
127
|
+
for event in msg.events
|
|
128
|
+
if event.type != VirtualThread.EVENT_TYPE
|
|
129
|
+
and event.type in event_types_in_virtual_thread
|
|
130
|
+
]
|
|
131
|
+
if VirtualThread.EVENT_TYPE not in event_types_in_virtual_thread:
|
|
132
|
+
# An event has been tagged with the virtual thread ID
|
|
133
|
+
# but the message itself is not in the virtual thread
|
|
134
|
+
# Somewhat pathological case, probably shouldn't happen
|
|
135
|
+
# Set the message text to empty string
|
|
136
|
+
msg.text = ""
|
|
137
|
+
return virtual_thread_content
|
|
45
138
|
|
|
46
139
|
def save_annotation(self, annotation: AnnotationBatch) -> None:
|
|
47
140
|
"""Save an annotation to the Interactions API"""
|
|
@@ -79,11 +172,18 @@ class InteractionsService:
|
|
|
79
172
|
|
|
80
173
|
return res
|
|
81
174
|
|
|
82
|
-
def fetch_all_threads_by_channel(
|
|
175
|
+
def fetch_all_threads_by_channel(
|
|
176
|
+
self,
|
|
177
|
+
channel_id: str,
|
|
178
|
+
min_timestamp: str,
|
|
179
|
+
thread_event_types: Optional[list[str]] = None,
|
|
180
|
+
) -> dict:
|
|
83
181
|
"""Fetch a message from the Interactions API"""
|
|
84
182
|
message_url = f"{self.base_url}/interaction/v1/search/channel"
|
|
85
183
|
request_body = self._channel_lookup_request(
|
|
86
|
-
channel_id=channel_id,
|
|
184
|
+
channel_id=channel_id,
|
|
185
|
+
min_timestamp=min_timestamp,
|
|
186
|
+
thread_event_types=thread_event_types,
|
|
87
187
|
)
|
|
88
188
|
response = requests.post(
|
|
89
189
|
message_url,
|
|
@@ -95,7 +195,7 @@ class InteractionsService:
|
|
|
95
195
|
return response.json()
|
|
96
196
|
|
|
97
197
|
def fetch_thread_messages_and_events_for_message(
|
|
98
|
-
self, message_id: str, event_types:
|
|
198
|
+
self, message_id: str, event_types: List[str]
|
|
99
199
|
) -> ThreadRelationsResponse:
|
|
100
200
|
"""Fetch messages sorted by timestamp and events for agent context"""
|
|
101
201
|
message_url = f"{self.base_url}/interaction/v1/search/message"
|
|
@@ -166,7 +266,9 @@ class InteractionsService:
|
|
|
166
266
|
return response.json()
|
|
167
267
|
|
|
168
268
|
@staticmethod
|
|
169
|
-
def _channel_lookup_request(
|
|
269
|
+
def _channel_lookup_request(
|
|
270
|
+
channel_id: str, min_timestamp: str, thread_event_types: Optional[list[str]]
|
|
271
|
+
) -> dict:
|
|
170
272
|
"""Interaction service API request to get threads and messages for a channel"""
|
|
171
273
|
return {
|
|
172
274
|
"id": channel_id,
|
|
@@ -176,7 +278,8 @@ class InteractionsService:
|
|
|
176
278
|
"messages": {
|
|
177
279
|
"filter": {"min_timestamp": min_timestamp},
|
|
178
280
|
"apply_annotations_from_actors": ["*"],
|
|
179
|
-
}
|
|
281
|
+
},
|
|
282
|
+
"events": {"filter": {"type": thread_event_types or []}},
|
|
180
283
|
}
|
|
181
284
|
}
|
|
182
285
|
},
|
|
@@ -87,6 +87,7 @@ class ReturnedMessage(BaseModel):
|
|
|
87
87
|
message_id: Optional[str] = None
|
|
88
88
|
annotated_text: Optional[str] = None
|
|
89
89
|
events: List[Event] = Field(default_factory=list)
|
|
90
|
+
preceding_messages: List["ReturnedMessage"] = Field(default_factory=list)
|
|
90
91
|
thread_id: Optional[str] = None
|
|
91
92
|
channel_id: Optional[str] = None
|
|
92
93
|
annotations: List[Annotation] = Field(default_factory=list)
|
|
@@ -144,6 +145,19 @@ class ThreadRelationsResponse(BaseModel):
|
|
|
144
145
|
) # includes events associated with each message
|
|
145
146
|
|
|
146
147
|
|
|
148
|
+
class VirtualThread:
|
|
149
|
+
"""Virtuals threads are an event type used to sub-divide a thread into sb-conversations"""
|
|
150
|
+
|
|
151
|
+
# The type of event that represetns a virtual thread
|
|
152
|
+
EVENT_TYPE = "virtual_thread"
|
|
153
|
+
|
|
154
|
+
# Data field in the event that contains the ID of the virtual thread id
|
|
155
|
+
ID_FIELD = "virtual_thread_id"
|
|
156
|
+
|
|
157
|
+
# Data field in the event that contains the type of other events in the virtual thread
|
|
158
|
+
EVENT_TYPE_FIELD = "event_type"
|
|
159
|
+
|
|
160
|
+
|
|
147
161
|
def thread_message_lookup_request(message_id: str, event_type: str) -> dict:
|
|
148
162
|
"""retrieve messages and events for the thread associated with a message"""
|
|
149
163
|
return {
|
|
@@ -7,7 +7,7 @@ dev_requirements = ["mypy", "pytest", "black", "types-requests"]
|
|
|
7
7
|
|
|
8
8
|
setuptools.setup(
|
|
9
9
|
name="nora_lib",
|
|
10
|
-
version="0.
|
|
10
|
+
version="0.1.8",
|
|
11
11
|
description="For making and coordinating agents and tools",
|
|
12
12
|
url="https://github.com/allenai/nora_lib",
|
|
13
13
|
packages=setuptools.find_packages(exclude=(["tests"])),
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
from nora_lib.interactions.interactions_service import InteractionsService
|
|
4
|
+
from nora_lib.interactions.models import *
|
|
5
|
+
from uuid import uuid4
|
|
6
|
+
|
|
7
|
+
ACTOR = uuid4()
|
|
8
|
+
THREAD = str(uuid4())
|
|
9
|
+
CHANNEL = str(uuid4())
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _msg(text):
|
|
13
|
+
return Message(
|
|
14
|
+
message_id=str(uuid4()),
|
|
15
|
+
actor_id=ACTOR,
|
|
16
|
+
text=text,
|
|
17
|
+
channel_id=CHANNEL,
|
|
18
|
+
thread_id=THREAD,
|
|
19
|
+
surface=Surface.WEB,
|
|
20
|
+
ts=datetime.now(),
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _event(msg: Message, type: str, data: dict):
|
|
25
|
+
return Event(
|
|
26
|
+
type=type,
|
|
27
|
+
actor_id=ACTOR,
|
|
28
|
+
timestamp=datetime.now(),
|
|
29
|
+
text="",
|
|
30
|
+
data=data,
|
|
31
|
+
message_id=msg.message_id,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@unittest.skip("Requires a local instance of the interactions service")
|
|
36
|
+
class TestVirtualThreads(unittest.TestCase):
|
|
37
|
+
def setUp(self):
|
|
38
|
+
self.svc = InteractionsService("http://localhost:8090", 30, None)
|
|
39
|
+
|
|
40
|
+
def test_placeholder(self):
|
|
41
|
+
virtual_thread_1 = "virtual_thread_1"
|
|
42
|
+
virtual_thread_2 = "virtual_thread_2"
|
|
43
|
+
msg1 = _msg("Hi 1")
|
|
44
|
+
msg2 = _msg("Hi 2")
|
|
45
|
+
self.svc.save_message(msg1)
|
|
46
|
+
self.svc.save_message(msg2, virtual_thread_1)
|
|
47
|
+
event1 = _event(msg2, "event1", {})
|
|
48
|
+
event2 = _event(msg2, "event2", {})
|
|
49
|
+
event3 = _event(msg2, "event3", {})
|
|
50
|
+
self.svc.save_event(event1)
|
|
51
|
+
self.svc.save_event(event2, virtual_thread_1)
|
|
52
|
+
self.svc.save_event(event3, virtual_thread_2)
|
|
53
|
+
|
|
54
|
+
content = self.svc.get_virtual_thread_content(msg2.message_id, virtual_thread_1)
|
|
55
|
+
# Should only contain the one message tagged with virtual_thread_1
|
|
56
|
+
self.assertEqual([m.message_id for m in content], [msg2.message_id])
|
|
57
|
+
# Should only contain the events tagged with virtual_thread_1
|
|
58
|
+
self.assertEqual([e.type for e in content[0].events], [event2.type])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|