nora-lib-impl 1.0.0__py3-none-any.whl
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/__init__.py +0 -0
- nora_lib/impl/__init__.py +0 -0
- nora_lib/impl/context/__init__.py +0 -0
- nora_lib/impl/context/agent_context.py +43 -0
- nora_lib/impl/context/context_service.py +33 -0
- nora_lib/impl/context/models.py +11 -0
- nora_lib/impl/interactions/__init__.py +0 -0
- nora_lib/impl/interactions/interactions_service.py +561 -0
- nora_lib/impl/interactions/models.py +414 -0
- nora_lib/impl/interactions/step_progress.py +69 -0
- nora_lib/impl/pubsub.py +140 -0
- nora_lib/impl/tasks/__init__.py +0 -0
- nora_lib/impl/tasks/state.py +156 -0
- nora_lib/progress/__init__.py +0 -0
- nora_lib/progress/models.py +47 -0
- nora_lib/progress/reporter.py +122 -0
- nora_lib/py.typed +0 -0
- nora_lib/serializers.py +10 -0
- nora_lib/tasks/__init__.py +0 -0
- nora_lib/tasks/models.py +25 -0
- nora_lib/tasks/state.py +71 -0
- nora_lib_impl-1.0.0.dist-info/METADATA +20 -0
- nora_lib_impl-1.0.0.dist-info/RECORD +25 -0
- nora_lib_impl-1.0.0.dist-info/WHEEL +5 -0
- nora_lib_impl-1.0.0.dist-info/top_level.txt +1 -0
nora_lib/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from nora_lib.impl.interactions.models import Surface
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MessageAgentContext(BaseModel):
|
|
8
|
+
"""
|
|
9
|
+
Identifiers for the triggering user message
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
message_id: str
|
|
13
|
+
thread_id: str
|
|
14
|
+
channel_id: str
|
|
15
|
+
surface: Surface
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PubsubAgentContext(BaseModel):
|
|
19
|
+
"""
|
|
20
|
+
The pubsub namespace in which the Handler is running
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
base_url: str
|
|
24
|
+
namespace: str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ToolConfigAgentContext(BaseModel):
|
|
28
|
+
"""
|
|
29
|
+
The name of the tool config being used by the handler (dev, prod, demo, etc.)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
env: str
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class AgentContext(BaseModel):
|
|
36
|
+
"""
|
|
37
|
+
Information that needs to be passed from the Handler to tool agents
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
message: MessageAgentContext
|
|
41
|
+
pubsub: PubsubAgentContext
|
|
42
|
+
tool_config: ToolConfigAgentContext
|
|
43
|
+
step_id: Optional[str] = None
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from nora_lib.impl.interactions.interactions_service import InteractionsService
|
|
4
|
+
from nora_lib.impl.interactions.models import ReturnedMessage
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ContextService:
|
|
8
|
+
"""
|
|
9
|
+
Save and retrieve task agent context from interaction store
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
agent_actor_id: str, # uuid representing this agent in interaction store
|
|
15
|
+
interactions_base_url: str,
|
|
16
|
+
interactions_bearer_token: Optional[str],
|
|
17
|
+
timeout: int = 30,
|
|
18
|
+
):
|
|
19
|
+
# If no config is provided, load the configuration based on the environment
|
|
20
|
+
self.interactions_service = self._get_interactions_service(
|
|
21
|
+
interactions_base_url, interactions_bearer_token, timeout
|
|
22
|
+
)
|
|
23
|
+
self.agent_actor_id = agent_actor_id
|
|
24
|
+
|
|
25
|
+
def _get_interactions_service(self, url, token, timeout) -> InteractionsService:
|
|
26
|
+
return InteractionsService(url, timeout, token)
|
|
27
|
+
|
|
28
|
+
def get_message(self, message_id: str) -> str:
|
|
29
|
+
message: ReturnedMessage = self.interactions_service.get_message(message_id)
|
|
30
|
+
if message.annotated_text:
|
|
31
|
+
return message.annotated_text
|
|
32
|
+
else:
|
|
33
|
+
return message.text
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from pydantic import BaseModel, Field
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class WrappedTaskObject(BaseModel):
|
|
6
|
+
"""Encloses request or response object with additional metadata"""
|
|
7
|
+
|
|
8
|
+
message_id: str = Field(
|
|
9
|
+
description="id of originating message; key for istore retrieval"
|
|
10
|
+
)
|
|
11
|
+
data: dict = Field(description="Tool-defined request or response")
|
|
File without changes
|
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
import logging
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
from typing import Optional, List
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import boto3
|
|
10
|
+
from requests import Response
|
|
11
|
+
from requests.auth import AuthBase
|
|
12
|
+
from aws_requests_auth.aws_auth import AWSRequestsAuth
|
|
13
|
+
from typing import Dict, Any
|
|
14
|
+
|
|
15
|
+
from nora_lib.impl.interactions.models import (
|
|
16
|
+
AnnotationBatch,
|
|
17
|
+
StepCost,
|
|
18
|
+
Event,
|
|
19
|
+
EventType,
|
|
20
|
+
Message,
|
|
21
|
+
ReturnedMessage,
|
|
22
|
+
ReturnedEvent,
|
|
23
|
+
Thread,
|
|
24
|
+
ThreadRelationsResponse,
|
|
25
|
+
VirtualThread,
|
|
26
|
+
ThreadStatus,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class InteractionsService:
|
|
31
|
+
"""
|
|
32
|
+
Service which saves interactions to the Interactions API
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
base_url: str,
|
|
38
|
+
timeout: int = 30,
|
|
39
|
+
token: Optional[str] = None,
|
|
40
|
+
auth: Optional[AuthBase] = None,
|
|
41
|
+
) -> None:
|
|
42
|
+
self.base_url = base_url
|
|
43
|
+
self.timeout = timeout
|
|
44
|
+
if auth:
|
|
45
|
+
self.auth = auth
|
|
46
|
+
elif token:
|
|
47
|
+
self.auth = BearerAuth(token)
|
|
48
|
+
else:
|
|
49
|
+
raise Exception("Either token or auth must be provided")
|
|
50
|
+
|
|
51
|
+
def _post(self, url: str, json: Dict[str, Any]) -> Response:
|
|
52
|
+
return requests.post(
|
|
53
|
+
url,
|
|
54
|
+
json=json,
|
|
55
|
+
auth=self.auth,
|
|
56
|
+
timeout=self.timeout,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def save_message(
|
|
60
|
+
self, message: Message, virtual_thread_id: Optional[str] = None
|
|
61
|
+
) -> None:
|
|
62
|
+
"""
|
|
63
|
+
Save a message to the Interaction Store
|
|
64
|
+
:param virtual_thread_id: Optional ID of a virtual thread to associate with the message
|
|
65
|
+
"""
|
|
66
|
+
message_url = f"{self.base_url}/interaction/v1/message"
|
|
67
|
+
response = self._post(
|
|
68
|
+
message_url,
|
|
69
|
+
message.model_dump(),
|
|
70
|
+
)
|
|
71
|
+
response.raise_for_status()
|
|
72
|
+
if virtual_thread_id:
|
|
73
|
+
# Use an event to tag the message with the virtual thread ID
|
|
74
|
+
event = Event(
|
|
75
|
+
type=VirtualThread.EVENT_TYPE,
|
|
76
|
+
actor_id=message.actor_id,
|
|
77
|
+
message_id=message.message_id,
|
|
78
|
+
data={
|
|
79
|
+
VirtualThread.ID_FIELD: virtual_thread_id,
|
|
80
|
+
VirtualThread.EVENT_TYPE_FIELD: VirtualThread.EVENT_TYPE,
|
|
81
|
+
},
|
|
82
|
+
timestamp=message.ts,
|
|
83
|
+
)
|
|
84
|
+
self.save_event(event)
|
|
85
|
+
|
|
86
|
+
def save_event(self, event: Event, virtual_thread_id: Optional[str] = None) -> str:
|
|
87
|
+
"""
|
|
88
|
+
Save an event to the Interaction Store. Returns an event id.
|
|
89
|
+
:param virtual_thread_id: Optional ID of a virtual thread to associate with the event
|
|
90
|
+
"""
|
|
91
|
+
event_url = f"{self.base_url}/interaction/v1/event"
|
|
92
|
+
response = self._post(
|
|
93
|
+
event_url,
|
|
94
|
+
event.model_dump(),
|
|
95
|
+
)
|
|
96
|
+
response.raise_for_status()
|
|
97
|
+
if virtual_thread_id:
|
|
98
|
+
# Use an event to tag the event with the virtual thread ID
|
|
99
|
+
# Attach it to the same message as this event, along with the event type
|
|
100
|
+
event = Event(
|
|
101
|
+
type=VirtualThread.EVENT_TYPE,
|
|
102
|
+
actor_id=event.actor_id,
|
|
103
|
+
message_id=event.message_id,
|
|
104
|
+
data={
|
|
105
|
+
VirtualThread.ID_FIELD: virtual_thread_id,
|
|
106
|
+
VirtualThread.EVENT_TYPE_FIELD: event.type,
|
|
107
|
+
},
|
|
108
|
+
timestamp=event.timestamp,
|
|
109
|
+
)
|
|
110
|
+
self.save_event(event)
|
|
111
|
+
response_message = json.loads(response.text)
|
|
112
|
+
event_id = response_message["event_id"]
|
|
113
|
+
return event_id
|
|
114
|
+
|
|
115
|
+
def save_thread(self, thread: Thread) -> None:
|
|
116
|
+
"""Save a thread to the Interactions API"""
|
|
117
|
+
thread_url = f"{self.base_url}/interaction/v1/thread"
|
|
118
|
+
response = self._post(
|
|
119
|
+
thread_url,
|
|
120
|
+
thread.model_dump(),
|
|
121
|
+
)
|
|
122
|
+
response.raise_for_status()
|
|
123
|
+
|
|
124
|
+
def save_message_reaction(
|
|
125
|
+
self, message_id: str, reaction: str, actor_id: UUID
|
|
126
|
+
) -> str:
|
|
127
|
+
"""Save reaction as an event on a message, returns event id if successful"""
|
|
128
|
+
if reaction is None:
|
|
129
|
+
event = Event(
|
|
130
|
+
type=EventType.REACTION_REMOVED.value,
|
|
131
|
+
actor_id=actor_id,
|
|
132
|
+
timestamp=datetime.now(timezone.utc),
|
|
133
|
+
message_id=message_id,
|
|
134
|
+
)
|
|
135
|
+
else:
|
|
136
|
+
event = Event(
|
|
137
|
+
type=EventType.REACTION_ADDED.value,
|
|
138
|
+
actor_id=actor_id,
|
|
139
|
+
timestamp=datetime.now(timezone.utc),
|
|
140
|
+
text=reaction,
|
|
141
|
+
message_id=message_id,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return self.save_event(event)
|
|
145
|
+
|
|
146
|
+
def save_message_feedback(
|
|
147
|
+
self, message_id: str, feedback: str, actor_id: UUID
|
|
148
|
+
) -> str:
|
|
149
|
+
"""Save feedback as an event on a message, returns event id if successful"""
|
|
150
|
+
event = Event(
|
|
151
|
+
type=EventType.USER_FEEDBACK.value,
|
|
152
|
+
actor_id=actor_id,
|
|
153
|
+
timestamp=datetime.now(timezone.utc),
|
|
154
|
+
text=feedback,
|
|
155
|
+
message_id=message_id,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
return self.save_event(event)
|
|
159
|
+
|
|
160
|
+
def save_thread_feedback(
|
|
161
|
+
self, thread_id: str, feedback: str, actor_id: UUID
|
|
162
|
+
) -> str:
|
|
163
|
+
"""Save feedback as an event on a thread, returns event id if successful"""
|
|
164
|
+
event = Event(
|
|
165
|
+
type=EventType.USER_FEEDBACK_THREAD.value,
|
|
166
|
+
actor_id=actor_id,
|
|
167
|
+
timestamp=datetime.now(timezone.utc),
|
|
168
|
+
text=feedback,
|
|
169
|
+
thread_id=thread_id,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
return self.save_event(event)
|
|
173
|
+
|
|
174
|
+
def get_virtual_thread_content(
|
|
175
|
+
self, message_id: str, virtual_thread_id: str
|
|
176
|
+
) -> List[ReturnedMessage]:
|
|
177
|
+
"""Fetch all messages and events in a virtual thread
|
|
178
|
+
Returns all messages and events in the same thread as the given message,
|
|
179
|
+
but filtered to only include those associated with the given virtual thread.
|
|
180
|
+
:param message_id: The ID of a message in the virtual thread
|
|
181
|
+
:param virtual_thread_id: The ID of the virtual thread
|
|
182
|
+
"""
|
|
183
|
+
message_search_url = f"{self.base_url}/interaction/v1/search/message"
|
|
184
|
+
# Fetch all events and filter on the client side
|
|
185
|
+
# Need an IStore schema change to do this server-side
|
|
186
|
+
request_body = {
|
|
187
|
+
"id": message_id,
|
|
188
|
+
"relations": {
|
|
189
|
+
"preceding_messages": {
|
|
190
|
+
"max": 100,
|
|
191
|
+
"relations": {"events": {}},
|
|
192
|
+
},
|
|
193
|
+
"events": {},
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
response = self._post(
|
|
198
|
+
message_search_url,
|
|
199
|
+
request_body,
|
|
200
|
+
)
|
|
201
|
+
response.raise_for_status()
|
|
202
|
+
result = ReturnedMessage.model_validate(response.json()["message"])
|
|
203
|
+
all_messages = result.preceding_messages + [result]
|
|
204
|
+
virtual_thread_content = []
|
|
205
|
+
for msg in all_messages:
|
|
206
|
+
event_types_in_virtual_thread = set(
|
|
207
|
+
event.data[VirtualThread.EVENT_TYPE_FIELD]
|
|
208
|
+
for event in msg.events
|
|
209
|
+
if event.type == VirtualThread.EVENT_TYPE
|
|
210
|
+
and event.data.get(VirtualThread.ID_FIELD) == virtual_thread_id
|
|
211
|
+
)
|
|
212
|
+
if not event_types_in_virtual_thread:
|
|
213
|
+
continue
|
|
214
|
+
virtual_thread_content.append(msg)
|
|
215
|
+
msg.events = [
|
|
216
|
+
event
|
|
217
|
+
for event in msg.events
|
|
218
|
+
if event.type != VirtualThread.EVENT_TYPE
|
|
219
|
+
and event.type in event_types_in_virtual_thread
|
|
220
|
+
]
|
|
221
|
+
if VirtualThread.EVENT_TYPE not in event_types_in_virtual_thread:
|
|
222
|
+
# An event has been tagged with the virtual thread ID
|
|
223
|
+
# but the message itself is not in the virtual thread
|
|
224
|
+
# Somewhat pathological case, probably shouldn't happen
|
|
225
|
+
# Set the message text to empty string
|
|
226
|
+
msg.text = ""
|
|
227
|
+
return virtual_thread_content
|
|
228
|
+
|
|
229
|
+
def save_annotation(self, annotation: AnnotationBatch) -> None:
|
|
230
|
+
"""Save an annotation to the Interactions API"""
|
|
231
|
+
annotation_url = f"{self.base_url}/interaction/v1/annotation"
|
|
232
|
+
response = self._post(
|
|
233
|
+
annotation_url,
|
|
234
|
+
annotation.model_dump(),
|
|
235
|
+
)
|
|
236
|
+
response.raise_for_status()
|
|
237
|
+
|
|
238
|
+
def get_message(self, message_id: str) -> ReturnedMessage:
|
|
239
|
+
"""Fetch a message from the Interactions API"""
|
|
240
|
+
message_url = f"{self.base_url}/interaction/v1/search/message"
|
|
241
|
+
request_body = {
|
|
242
|
+
"id": message_id,
|
|
243
|
+
"relations": {"thread": {}, "channel": {}, "events": {}, "annotations": {}},
|
|
244
|
+
}
|
|
245
|
+
response = self._post(
|
|
246
|
+
message_url,
|
|
247
|
+
request_body,
|
|
248
|
+
)
|
|
249
|
+
response.raise_for_status()
|
|
250
|
+
res_dict = response.json()["message"]
|
|
251
|
+
res = ReturnedMessage.model_validate(res_dict)
|
|
252
|
+
|
|
253
|
+
# thread_id and channel_id are for some reason nested in the response
|
|
254
|
+
if not res.thread_id:
|
|
255
|
+
res.thread_id = res_dict.get("thread", {}).get("thread_id")
|
|
256
|
+
if not res.channel_id:
|
|
257
|
+
res.channel_id = res_dict.get("channel", {}).get("channel_id")
|
|
258
|
+
|
|
259
|
+
return res
|
|
260
|
+
|
|
261
|
+
def get_event(self, event_id: str) -> ReturnedEvent:
|
|
262
|
+
"""Fetch an event from the Interactions API"""
|
|
263
|
+
event_url = f"{self.base_url}/interaction/v1/search/event"
|
|
264
|
+
request_body = {
|
|
265
|
+
"id": event_id,
|
|
266
|
+
}
|
|
267
|
+
response = self._post(
|
|
268
|
+
event_url,
|
|
269
|
+
request_body,
|
|
270
|
+
)
|
|
271
|
+
response.raise_for_status()
|
|
272
|
+
res_dict = response.json()["events"][0]
|
|
273
|
+
res = ReturnedEvent.model_validate(res_dict)
|
|
274
|
+
return res
|
|
275
|
+
|
|
276
|
+
def fetch_all_threads_by_channel(
|
|
277
|
+
self,
|
|
278
|
+
channel_id: str,
|
|
279
|
+
min_timestamp: Optional[str] = None,
|
|
280
|
+
thread_event_types: Optional[list[str]] = None,
|
|
281
|
+
most_recent: Optional[int] = None,
|
|
282
|
+
) -> dict:
|
|
283
|
+
"""Fetch a message from the Interactions API"""
|
|
284
|
+
message_url = f"{self.base_url}/interaction/v1/search/channel"
|
|
285
|
+
request_body = self._channel_lookup_request(
|
|
286
|
+
channel_id=channel_id,
|
|
287
|
+
min_timestamp=min_timestamp,
|
|
288
|
+
thread_event_types=thread_event_types,
|
|
289
|
+
most_recent=most_recent,
|
|
290
|
+
)
|
|
291
|
+
response = self._post(
|
|
292
|
+
message_url,
|
|
293
|
+
request_body,
|
|
294
|
+
)
|
|
295
|
+
response.raise_for_status()
|
|
296
|
+
return response.json()
|
|
297
|
+
|
|
298
|
+
def fetch_thread_messages_and_events_for_message(
|
|
299
|
+
self,
|
|
300
|
+
message_id: str,
|
|
301
|
+
event_types: List[str],
|
|
302
|
+
min_timestamp: Optional[str] = None,
|
|
303
|
+
most_recent: Optional[int] = None,
|
|
304
|
+
) -> ThreadRelationsResponse:
|
|
305
|
+
"""Fetch messages sorted by timestamp and events for agent context"""
|
|
306
|
+
message_url = f"{self.base_url}/interaction/v1/search/message"
|
|
307
|
+
request_body = self._thread_lookup_request(
|
|
308
|
+
message_id,
|
|
309
|
+
event_types=event_types,
|
|
310
|
+
min_timestamp=min_timestamp,
|
|
311
|
+
most_recent=most_recent,
|
|
312
|
+
)
|
|
313
|
+
response = self._post(
|
|
314
|
+
message_url,
|
|
315
|
+
request_body,
|
|
316
|
+
)
|
|
317
|
+
response.raise_for_status()
|
|
318
|
+
json_response = response.json()
|
|
319
|
+
|
|
320
|
+
return ThreadRelationsResponse.model_validate(
|
|
321
|
+
json_response.get("message", {}).get("thread", {})
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
def fetch_messages_and_events_for_thread(
|
|
325
|
+
self,
|
|
326
|
+
thread_id: str,
|
|
327
|
+
event_type: Optional[str] = None,
|
|
328
|
+
min_timestamp: Optional[str] = None,
|
|
329
|
+
) -> dict:
|
|
330
|
+
"""Fetch messages and events for the given thread from the Interactions API"""
|
|
331
|
+
thread_search_url = f"{self.base_url}/interaction/v1/search/thread"
|
|
332
|
+
message_query = {
|
|
333
|
+
"filter": {"min_timestamp": min_timestamp} if min_timestamp else None,
|
|
334
|
+
"apply_annotations_from_actors": ["*"],
|
|
335
|
+
}
|
|
336
|
+
request_body = {
|
|
337
|
+
"id": thread_id,
|
|
338
|
+
"relations": {
|
|
339
|
+
"messages": message_query,
|
|
340
|
+
"events": {"filter": {"type": event_type}} if event_type else {},
|
|
341
|
+
},
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
response = self._post(
|
|
345
|
+
thread_search_url,
|
|
346
|
+
request_body,
|
|
347
|
+
)
|
|
348
|
+
response.raise_for_status()
|
|
349
|
+
return response.json()
|
|
350
|
+
|
|
351
|
+
def fetch_events_for_message(
|
|
352
|
+
self,
|
|
353
|
+
message_id: str,
|
|
354
|
+
event_type: Optional[str] = None,
|
|
355
|
+
) -> dict:
|
|
356
|
+
"""Fetch messages and events for the thread containing a given message from the Interactions API"""
|
|
357
|
+
message_search_url = f"{self.base_url}/interaction/v1/search/message"
|
|
358
|
+
request_body = {
|
|
359
|
+
"id": message_id,
|
|
360
|
+
"relations": {
|
|
361
|
+
"events": {"filter": {"type": event_type}} if event_type else {},
|
|
362
|
+
},
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
response = self._post(
|
|
366
|
+
message_search_url,
|
|
367
|
+
request_body,
|
|
368
|
+
)
|
|
369
|
+
response.raise_for_status()
|
|
370
|
+
return response.json()
|
|
371
|
+
|
|
372
|
+
def fetch_all_by_channel(
|
|
373
|
+
self,
|
|
374
|
+
channel_id: str,
|
|
375
|
+
min_timestamp: Optional[str] = None,
|
|
376
|
+
event_types: Optional[List[str]] = None,
|
|
377
|
+
num_most_recent_threads: Optional[int] = None,
|
|
378
|
+
num_most_recent_messages_per_thread: Optional[int] = None,
|
|
379
|
+
num_oldest_messages_per_thread: Optional[int] = None,
|
|
380
|
+
thread_status: List[ThreadStatus] = [ThreadStatus.ACTIVE],
|
|
381
|
+
) -> dict:
|
|
382
|
+
"""
|
|
383
|
+
Fetch all threads, messages, and events including nested ones for a given channel
|
|
384
|
+
"""
|
|
385
|
+
channel_search_url = f"{self.base_url}/interaction/v1/search/channel"
|
|
386
|
+
thread_filter_query = {
|
|
387
|
+
"status": thread_status,
|
|
388
|
+
"min_timestamp": min_timestamp if min_timestamp else None,
|
|
389
|
+
"most_recent": num_most_recent_threads if num_most_recent_threads else None,
|
|
390
|
+
}
|
|
391
|
+
event_query = {"filter": None if event_types is None else {"type": event_types}}
|
|
392
|
+
message_filter_query = {
|
|
393
|
+
"min_timestamp": min_timestamp if min_timestamp else None,
|
|
394
|
+
"most_recent": (
|
|
395
|
+
num_most_recent_messages_per_thread
|
|
396
|
+
if num_most_recent_messages_per_thread
|
|
397
|
+
else None
|
|
398
|
+
),
|
|
399
|
+
"oldest": (
|
|
400
|
+
num_oldest_messages_per_thread
|
|
401
|
+
if num_oldest_messages_per_thread
|
|
402
|
+
else None
|
|
403
|
+
),
|
|
404
|
+
}
|
|
405
|
+
message_query = {
|
|
406
|
+
"relations": {"events": event_query, "annotations:": {}},
|
|
407
|
+
"filter": message_filter_query,
|
|
408
|
+
"apply_annotations_from_actors": ["*"],
|
|
409
|
+
}
|
|
410
|
+
request_body = {
|
|
411
|
+
"id": channel_id,
|
|
412
|
+
"relations": {
|
|
413
|
+
"threads": {
|
|
414
|
+
"filter": thread_filter_query,
|
|
415
|
+
"relations": {
|
|
416
|
+
"messages": message_query,
|
|
417
|
+
"events": event_query,
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
}
|
|
422
|
+
response = self._post(
|
|
423
|
+
channel_search_url,
|
|
424
|
+
request_body,
|
|
425
|
+
)
|
|
426
|
+
response.raise_for_status()
|
|
427
|
+
return response.json()
|
|
428
|
+
|
|
429
|
+
def fetch_all_by_thread(
|
|
430
|
+
self,
|
|
431
|
+
thread_id: str,
|
|
432
|
+
min_timestamp: Optional[str] = None,
|
|
433
|
+
event_types: Optional[List[str]] = None,
|
|
434
|
+
most_recent: Optional[int] = None,
|
|
435
|
+
) -> dict:
|
|
436
|
+
"""
|
|
437
|
+
Fetch all messages and events including nested ones for a given thread
|
|
438
|
+
"""
|
|
439
|
+
thread_search_url = f"{self.base_url}/interaction/v1/search/thread"
|
|
440
|
+
event_query = {"filter": None if event_types is None else {"type": event_types}}
|
|
441
|
+
message_filter_query = {
|
|
442
|
+
"min_timestamp": min_timestamp if min_timestamp else None,
|
|
443
|
+
"most_recent": most_recent if most_recent else None,
|
|
444
|
+
}
|
|
445
|
+
message_query = {
|
|
446
|
+
"relations": {"events": event_query, "annotations:": {}},
|
|
447
|
+
"filter": message_filter_query,
|
|
448
|
+
"apply_annotations_from_actors": ["*"],
|
|
449
|
+
}
|
|
450
|
+
request_body = {
|
|
451
|
+
"id": thread_id,
|
|
452
|
+
"relations": {
|
|
453
|
+
"messages": message_query,
|
|
454
|
+
"events": event_query,
|
|
455
|
+
},
|
|
456
|
+
}
|
|
457
|
+
response = self._post(
|
|
458
|
+
thread_search_url,
|
|
459
|
+
request_body,
|
|
460
|
+
)
|
|
461
|
+
response.raise_for_status()
|
|
462
|
+
return response.json()
|
|
463
|
+
|
|
464
|
+
def report_cost(self, step_cost: StepCost) -> Optional[str]:
|
|
465
|
+
"""Save a cost report to the Interactions Store. Returning event id"""
|
|
466
|
+
try:
|
|
467
|
+
return self.save_event(step_cost.to_event())
|
|
468
|
+
except requests.exceptions.HTTPError as e:
|
|
469
|
+
if e.response.status_code == 404:
|
|
470
|
+
logging.warning(
|
|
471
|
+
f"Cannot find message id {step_cost.message_id} to attach cost report to."
|
|
472
|
+
)
|
|
473
|
+
return None
|
|
474
|
+
else:
|
|
475
|
+
raise e
|
|
476
|
+
|
|
477
|
+
@staticmethod
|
|
478
|
+
def _channel_lookup_request(
|
|
479
|
+
channel_id: str,
|
|
480
|
+
min_timestamp: Optional[str] = None,
|
|
481
|
+
thread_event_types: Optional[list[str]] = None,
|
|
482
|
+
most_recent: Optional[int] = None,
|
|
483
|
+
) -> dict:
|
|
484
|
+
"""Interaction service API request to get threads and messages for a channel"""
|
|
485
|
+
message_filter_query = {
|
|
486
|
+
"min_timestamp": min_timestamp if min_timestamp else None,
|
|
487
|
+
"most_recent": most_recent if most_recent else None,
|
|
488
|
+
}
|
|
489
|
+
return {
|
|
490
|
+
"id": channel_id,
|
|
491
|
+
"relations": {
|
|
492
|
+
"threads": {
|
|
493
|
+
"relations": {
|
|
494
|
+
"messages": {
|
|
495
|
+
"filter": message_filter_query,
|
|
496
|
+
"apply_annotations_from_actors": ["*"],
|
|
497
|
+
},
|
|
498
|
+
"events": {"filter": {"type": thread_event_types or []}},
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
},
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
@staticmethod
|
|
505
|
+
def _thread_lookup_request(
|
|
506
|
+
message_id: str,
|
|
507
|
+
event_types: list[str],
|
|
508
|
+
min_timestamp: Optional[str] = None,
|
|
509
|
+
most_recent: Optional[int] = None,
|
|
510
|
+
) -> dict:
|
|
511
|
+
"""will return all messages for the thread containing the given message and events associated with each message"""
|
|
512
|
+
message_filter_query = {
|
|
513
|
+
"min_timestamp": min_timestamp if min_timestamp else None,
|
|
514
|
+
"most_recent": most_recent if most_recent else None,
|
|
515
|
+
}
|
|
516
|
+
return {
|
|
517
|
+
"id": message_id,
|
|
518
|
+
"relations": {
|
|
519
|
+
"thread": {
|
|
520
|
+
"relations": {
|
|
521
|
+
"messages": {
|
|
522
|
+
"filter": message_filter_query,
|
|
523
|
+
"relations": {"events": {"filter": {"type": event_types}}},
|
|
524
|
+
"apply_annotations_from_actors": ["*"],
|
|
525
|
+
},
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
},
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
@staticmethod
|
|
532
|
+
def fetch_bearer_token(secret_id: str) -> str:
|
|
533
|
+
secrets_manager = boto3.client("secretsmanager", region_name="us-west-2")
|
|
534
|
+
return json.loads(
|
|
535
|
+
secrets_manager.get_secret_value(SecretId=secret_id)["SecretString"]
|
|
536
|
+
)["token"]
|
|
537
|
+
|
|
538
|
+
@staticmethod
|
|
539
|
+
def from_env() -> "InteractionsService":
|
|
540
|
+
"""Load the configuration based on the environment."""
|
|
541
|
+
url = os.getenv(
|
|
542
|
+
"INTERACTION_STORE_URL",
|
|
543
|
+
"https://nora-retrieval-public.prod.s2.allenai.org",
|
|
544
|
+
)
|
|
545
|
+
token = os.getenv(
|
|
546
|
+
"INTERACTION_STORE_TOKEN",
|
|
547
|
+
InteractionsService.fetch_bearer_token(
|
|
548
|
+
"nora/prod/interaction-bearer-token"
|
|
549
|
+
),
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
return InteractionsService(base_url=url, token=token)
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
class BearerAuth(requests.auth.AuthBase):
|
|
556
|
+
def __init__(self, token):
|
|
557
|
+
self.token = token
|
|
558
|
+
|
|
559
|
+
def __call__(self, r):
|
|
560
|
+
r.headers["Authorization"] = f"Bearer {self.token}"
|
|
561
|
+
return r
|