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.
Files changed (25) hide show
  1. {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/PKG-INFO +1 -1
  2. {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/nora_lib/interactions/interactions_service.py +113 -10
  3. {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/nora_lib/interactions/models.py +14 -0
  4. {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/nora_lib.egg-info/PKG-INFO +1 -1
  5. {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/nora_lib.egg-info/SOURCES.txt +1 -0
  6. {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/setup.py +1 -1
  7. nora_lib-0.1.8/tests/test_virtual_threads.py +58 -0
  8. {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/README.md +0 -0
  9. {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/nora_lib/__init__.py +0 -0
  10. {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/nora_lib/context/__init__.py +0 -0
  11. {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/nora_lib/context/context_service.py +0 -0
  12. {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/nora_lib/context/models.py +0 -0
  13. {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/nora_lib/interactions/__init__.py +0 -0
  14. {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/nora_lib/py.typed +0 -0
  15. {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/nora_lib/tasks/__init__.py +0 -0
  16. {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/nora_lib/tasks/models.py +0 -0
  17. {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/nora_lib/tasks/state.py +0 -0
  18. {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/nora_lib.egg-info/dependency_links.txt +0 -0
  19. {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/nora_lib.egg-info/requires.txt +0 -0
  20. {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/nora_lib.egg-info/top_level.txt +0 -0
  21. {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/pyproject.toml +0 -0
  22. {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/setup.cfg +0 -0
  23. {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/tests/tasks/__init__.py +0 -0
  24. {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/tests/tasks/test_state.py +0 -0
  25. {nora_lib-0.0.7.dev2 → nora_lib-0.1.8}/tests/test_placeholder.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: nora_lib
3
- Version: 0.0.7.dev2
3
+ Version: 0.1.8
4
4
  Summary: For making and coordinating agents and tools
5
5
  Home-page: https://github.com/allenai/nora_lib
6
6
  Requires-Python: >=3.9
@@ -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(self, message: Message) -> None:
25
- """Save a message to the Interactions API"""
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
- """Save an event to the Interactions API"""
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(self, channel_id: str, min_timestamp: str) -> dict:
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, min_timestamp=min_timestamp
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: list[str]
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(channel_id: str, min_timestamp: str) -> dict:
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 {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: nora_lib
3
- Version: 0.0.7.dev2
3
+ Version: 0.1.8
4
4
  Summary: For making and coordinating agents and tools
5
5
  Home-page: https://github.com/allenai/nora_lib
6
6
  Requires-Python: >=3.9
@@ -18,5 +18,6 @@ nora_lib/tasks/__init__.py
18
18
  nora_lib/tasks/models.py
19
19
  nora_lib/tasks/state.py
20
20
  tests/test_placeholder.py
21
+ tests/test_virtual_threads.py
21
22
  tests/tasks/__init__.py
22
23
  tests/tasks/test_state.py
@@ -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.0.7.dev2",
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