recallrai 0.2.0__py3-none-any.whl → 0.3.1__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.

Potentially problematic release.


This version of recallrai might be problematic. Click here for more details.

@@ -0,0 +1,189 @@
1
+ """
2
+ Merge conflict management functionality for the RecallrAI SDK.
3
+ """
4
+
5
+ from typing import List
6
+ from .utils import HTTPClient
7
+ from .models import (
8
+ MergeConflictModel,
9
+ MergeConflictStatus,
10
+ MergeConflictAnswer,
11
+ )
12
+ from .exceptions import (
13
+ UserNotFoundError,
14
+ MergeConflictNotFoundError,
15
+ MergeConflictAlreadyResolvedError,
16
+ MergeConflictInvalidQuestionsError,
17
+ MergeConflictMissingAnswersError,
18
+ MergeConflictInvalidAnswerError,
19
+ RecallrAIError
20
+ )
21
+ from logging import getLogger
22
+
23
+ logger = getLogger(__name__)
24
+
25
+
26
+ class MergeConflict:
27
+ """
28
+ Represents a merge conflict in the RecallrAI system.
29
+
30
+ This class provides methods for inspecting and resolving merge conflicts
31
+ that occur when new memories conflict with existing ones.
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ http_client: HTTPClient,
37
+ user_id: str,
38
+ conflict_data: MergeConflictModel,
39
+ ):
40
+ """
41
+ Initialize a merge conflict.
42
+
43
+ Args:
44
+ http_client: HTTP client for API communication.
45
+ user_id: User ID who owns this conflict.
46
+ conflict_data: Merge conflict data model.
47
+ """
48
+ self._http = http_client
49
+ self.user_id = user_id
50
+ self._conflict_data = conflict_data
51
+
52
+ # Expose key properties for easy access
53
+ self.conflict_id = conflict_data.id
54
+ self.status = conflict_data.status
55
+ self.new_memory_content = conflict_data.new_memory_content
56
+ self.conflicting_memories = conflict_data.conflicting_memories
57
+ self.clarifying_questions = conflict_data.clarifying_questions
58
+ self.created_at = conflict_data.created_at
59
+ self.resolved_at = conflict_data.resolved_at
60
+ self.resolution_data = conflict_data.resolution_data
61
+
62
+ def resolve(self, answers: List[MergeConflictAnswer]) -> None:
63
+ """
64
+ Resolve this merge conflict by providing answers to clarifying questions.
65
+
66
+ Args:
67
+ answers: List of answers to the clarifying questions.
68
+
69
+ Raises:
70
+ UserNotFoundError: If the user is not found.
71
+ MergeConflictNotFoundError: If the merge conflict is not found.
72
+ MergeConflictAlreadyResolvedError: If the conflict is already resolved.
73
+ MergeConflictInvalidQuestionsError: If the provided questions don't match the original questions.
74
+ MergeConflictMissingAnswersError: If not all required questions have been answered.
75
+ MergeConflictInvalidAnswerError: If an answer is not a valid option for its question.
76
+ ValidationError: If the answers are invalid.
77
+ AuthenticationError: If the API key or project ID is invalid.
78
+ InternalServerError: If the server encounters an error.
79
+ NetworkError: If there are network issues.
80
+ TimeoutError: If the request times out.
81
+ RecallrAIError: For other API-related errors.
82
+ """
83
+ if self.status in [MergeConflictStatus.RESOLVED, MergeConflictStatus.FAILED]:
84
+ raise MergeConflictAlreadyResolvedError(
85
+ message=f"Merge conflict {self.conflict_id} is already resolved",
86
+ http_status=400
87
+ )
88
+
89
+ # Convert answers to the format expected by the API
90
+ answer_data = {
91
+ "question_answers": [
92
+ {
93
+ "question": answer.question,
94
+ "answer": answer.answer,
95
+ "message": answer.message,
96
+ }
97
+ for answer in answers
98
+ ]
99
+ }
100
+
101
+ response = self._http.post(
102
+ f"/api/v1/users/{self.user_id}/merge-conflicts/{self.conflict_id}/resolve",
103
+ data={"answers": answer_data},
104
+ )
105
+
106
+ if response.status_code == 404:
107
+ # Check if it's a user not found or conflict not found error
108
+ detail = response.json().get('detail', '')
109
+ if f"User {self.user_id} not found" in detail:
110
+ raise UserNotFoundError(message=detail, http_status=response.status_code)
111
+ else:
112
+ raise MergeConflictNotFoundError(message=detail, http_status=response.status_code)
113
+ elif response.status_code == 400:
114
+ detail = response.json().get('detail', '')
115
+ if "already resolved" in detail:
116
+ raise MergeConflictAlreadyResolvedError(message=detail, http_status=response.status_code)
117
+ elif "Invalid questions provided" in detail:
118
+ raise MergeConflictInvalidQuestionsError(
119
+ message=detail,
120
+ http_status=response.status_code
121
+ )
122
+ elif "Missing answers for the following questions" in detail:
123
+ raise MergeConflictMissingAnswersError(
124
+ message=detail,
125
+ http_status=response.status_code
126
+ )
127
+ elif "Invalid answer" in detail and "for question" in detail:
128
+ raise MergeConflictInvalidAnswerError(
129
+ message=detail,
130
+ http_status=response.status_code
131
+ )
132
+ else:
133
+ raise RecallrAIError(
134
+ message=detail,
135
+ http_status=response.status_code
136
+ )
137
+ elif response.status_code != 200:
138
+ raise RecallrAIError(
139
+ message=response.json().get('detail', 'Unknown error'),
140
+ http_status=response.status_code
141
+ )
142
+
143
+ # Update the conflict data with the response
144
+ updated_data = MergeConflictModel.from_api_response(response.json())
145
+ self._conflict_data = updated_data
146
+ self.status = updated_data.status
147
+ self.resolved_at = updated_data.resolved_at
148
+ self.resolution_data = updated_data.resolution_data
149
+
150
+ def refresh(self) -> None:
151
+ """
152
+ Refresh this merge conflict's data from the API.
153
+
154
+ Raises:
155
+ UserNotFoundError: If the user is not found.
156
+ MergeConflictNotFoundError: If the merge conflict is not found.
157
+ AuthenticationError: If the API key or project ID is invalid.
158
+ InternalServerError: If the server encounters an error.
159
+ NetworkError: If there are network issues.
160
+ TimeoutError: If the request times out.
161
+ RecallrAIError: For other API-related errors.
162
+ """
163
+ response = self._http.get(
164
+ f"/api/v1/users/{self.user_id}/merge-conflicts/{self.conflict_id}"
165
+ )
166
+
167
+ if response.status_code == 404:
168
+ # Check if it's a user not found or conflict not found error
169
+ detail = response.json().get('detail', '')
170
+ if f"User {self.user_id} not found" in detail:
171
+ raise UserNotFoundError(message=detail, http_status=response.status_code)
172
+ else:
173
+ raise MergeConflictNotFoundError(message=detail, http_status=response.status_code)
174
+ elif response.status_code != 200:
175
+ raise RecallrAIError(
176
+ message=response.json().get('detail', 'Unknown error'),
177
+ http_status=response.status_code
178
+ )
179
+
180
+ # Update with fresh data
181
+ updated_data = MergeConflictModel.from_api_response(response.json())
182
+ self._conflict_data = updated_data
183
+ self.status = updated_data.status
184
+ self.resolved_at = updated_data.resolved_at
185
+ self.resolution_data = updated_data.resolution_data
186
+
187
+ def __repr__(self) -> str:
188
+ """Return a string representation of the merge conflict."""
189
+ return f"MergeConflict(id='{self.conflict_id}', status='{self.status}', user_id='{self.user_id}')"
@@ -1,16 +1,49 @@
1
1
  """
2
2
  Models used in the SDK.
3
3
  """
4
- from .session import Context, Message, MessageRole, SessionModel, SessionList, SessionStatus
5
- from .user import UserModel, UserList
4
+
5
+ from .user import (
6
+ UserModel,
7
+ UserList,
8
+ UserMemoriesList,
9
+ UserMessage,
10
+ UserMessagesList,
11
+ UserMemoryItem,
12
+ MemoryVersionInfo,
13
+ MemoryRelationship,
14
+ )
15
+ from .session import SessionModel, SessionList, SessionMessagesList, MessageRole, SessionStatus, Context, RecallStrategy
16
+ from .merge_conflict import (
17
+ MergeConflictModel,
18
+ MergeConflictList,
19
+ MergeConflictStatus,
20
+ MergeConflictMemory,
21
+ MergeConflictQuestion,
22
+ MergeConflictAnswer,
23
+ )
6
24
 
7
25
  __all__ = [
8
- "UserModel",
26
+ "UserModel",
9
27
  "UserList",
28
+ "UserMemoriesList",
29
+ "UserMessage",
30
+ "UserMessagesList",
31
+ "UserMemoryItem",
32
+ "MemoryVersionInfo",
33
+ "MemoryRelationship",
34
+
10
35
  "SessionModel",
11
36
  "SessionList",
12
- "SessionStatus",
13
- "Message",
37
+ "SessionMessagesList",
14
38
  "MessageRole",
39
+ "SessionStatus",
15
40
  "Context",
41
+ "RecallStrategy",
42
+
43
+ "MergeConflictModel",
44
+ "MergeConflictList",
45
+ "MergeConflictStatus",
46
+ "MergeConflictMemory",
47
+ "MergeConflictQuestion",
48
+ "MergeConflictAnswer",
16
49
  ]
@@ -0,0 +1,151 @@
1
+ """
2
+ Merge conflict-related data models for the RecallrAI SDK.
3
+ """
4
+
5
+ import enum
6
+ from datetime import datetime
7
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional
8
+ from pydantic import BaseModel, Field
9
+ from ..utils import HTTPClient
10
+
11
+ if TYPE_CHECKING:
12
+ from ..merge_conflict import MergeConflict
13
+
14
+
15
+ class MergeConflictStatus(str, enum.Enum):
16
+ """
17
+ Status of a merge conflict.
18
+ """
19
+ PENDING = "PENDING"
20
+ IN_QUEUE = "IN_QUEUE"
21
+ RESOLVING = "RESOLVING"
22
+ RESOLVED = "RESOLVED"
23
+ FAILED = "FAILED"
24
+
25
+
26
+ class MergeConflictMemory(BaseModel):
27
+ """
28
+ Represents a memory involved in a merge conflict.
29
+ """
30
+ content: str = Field(..., description="Content of the conflicting memory.")
31
+ reason: str = Field(..., description="Reason why this memory conflicts.")
32
+
33
+ class Config:
34
+ """Pydantic configuration."""
35
+ frozen = True
36
+
37
+
38
+ class MergeConflictQuestion(BaseModel):
39
+ """
40
+ Represents a clarifying question for merge conflict resolution.
41
+ """
42
+ question: str = Field(..., description="The clarifying question.")
43
+ options: List[str] = Field(..., description="Available answer options.")
44
+
45
+ class Config:
46
+ """Pydantic configuration."""
47
+ frozen = True
48
+
49
+
50
+ class MergeConflictAnswer(BaseModel):
51
+ """
52
+ Represents an answer to a clarifying question.
53
+ """
54
+ question: str = Field(..., description="The question being answered.")
55
+ answer: str = Field(..., description="The selected answer.")
56
+ message: Optional[str] = Field(None, description="Optional additional message.")
57
+
58
+ class Config:
59
+ """Pydantic configuration."""
60
+ frozen = True
61
+
62
+
63
+ class MergeConflictModel(BaseModel):
64
+ """
65
+ Represents a merge conflict in the RecallrAI system.
66
+ """
67
+ id: str = Field(..., description="Unique identifier for the merge conflict.")
68
+ custom_user_id: str = Field(..., description="User ID who owns this conflict.")
69
+ project_user_session_id: str = Field(..., description="Session ID where the conflict occurred.")
70
+ new_memory_content: str = Field(..., description="New memory content that caused the conflict.")
71
+ conflicting_memories: List[MergeConflictMemory] = Field(..., description="Existing memories that conflict.")
72
+ clarifying_questions: List[MergeConflictQuestion] = Field(..., description="Questions to resolve the conflict.")
73
+ status: MergeConflictStatus = Field(..., description="Current status of the conflict.")
74
+ resolution_data: Optional[Dict[str, Any]] = Field(None, description="Resolution data if resolved.")
75
+ created_at: datetime = Field(..., description="When the conflict was created.")
76
+ resolved_at: Optional[datetime] = Field(None, description="When the conflict was resolved.")
77
+
78
+ class Config:
79
+ """Pydantic configuration."""
80
+ frozen = True
81
+ json_encoders = {
82
+ datetime: lambda dt: dt.isoformat()
83
+ }
84
+
85
+ @classmethod
86
+ def from_api_response(cls, data: Dict[str, Any]) -> "MergeConflictModel":
87
+ """
88
+ Create a MergeConflictModel instance from an API response.
89
+
90
+ Args:
91
+ data: API response data.
92
+
93
+ Returns:
94
+ A MergeConflictModel instance.
95
+ """
96
+ conflict_data = data.get("conflict", data)
97
+
98
+ return cls(
99
+ id=conflict_data["id"],
100
+ custom_user_id=conflict_data["custom_user_id"],
101
+ project_user_session_id=conflict_data["project_user_session_id"],
102
+ new_memory_content=conflict_data["new_memory_content"],
103
+ conflicting_memories=[
104
+ MergeConflictMemory(**memory) for memory in conflict_data["conflicting_memories"]
105
+ ],
106
+ clarifying_questions=[
107
+ MergeConflictQuestion(**question) for question in conflict_data["clarifying_questions"]
108
+ ],
109
+ status=MergeConflictStatus(conflict_data["status"]),
110
+ resolution_data=conflict_data.get("resolution_data"),
111
+ created_at=conflict_data["created_at"],
112
+ resolved_at=conflict_data.get("resolved_at"),
113
+ )
114
+
115
+
116
+ class MergeConflictList(BaseModel):
117
+ """
118
+ Represents a paginated list of merge conflicts.
119
+ """
120
+ conflicts: List["MergeConflict"] = Field(..., description="List of merge conflicts.")
121
+ total: int = Field(..., description="Total number of conflicts.")
122
+ has_more: bool = Field(..., description="Whether there are more conflicts to fetch.")
123
+
124
+ class Config:
125
+ """Pydantic configuration."""
126
+ frozen = True
127
+ arbitrary_types_allowed = True
128
+
129
+ @classmethod
130
+ def from_api_response(cls, data: Dict[str, Any], http_client: HTTPClient, user_id: str) -> "MergeConflictList":
131
+ """
132
+ Create a MergeConflictList instance from an API response.
133
+
134
+ Args:
135
+ data: API response data.
136
+ http_client: HTTP client for making API requests.
137
+ user_id: User ID who owns these conflicts.
138
+
139
+ Returns:
140
+ A MergeConflictList instance.
141
+ """
142
+ from ..merge_conflict import MergeConflict
143
+
144
+ return cls(
145
+ conflicts=[
146
+ MergeConflict(http_client, user_id, MergeConflictModel.from_api_response(conflict))
147
+ for conflict in data["conflicts"]
148
+ ],
149
+ total=data["total"],
150
+ has_more=data["has_more"],
151
+ )
@@ -2,12 +2,13 @@
2
2
  Session-related data models for the RecallrAI SDK.
3
3
  """
4
4
 
5
- import enum, uuid
5
+ import enum
6
6
  from datetime import datetime
7
- from typing import Any, Dict, List
8
-
7
+ from typing import TYPE_CHECKING, Any, Dict, List
9
8
  from pydantic import BaseModel, Field
10
-
9
+ from ..utils import HTTPClient
10
+ if TYPE_CHECKING:
11
+ from ..session import Session
11
12
 
12
13
  class MessageRole(str, enum.Enum):
13
14
  """
@@ -21,9 +22,9 @@ class Message(BaseModel):
21
22
  """
22
23
  Represents a message in a conversation session.
23
24
  """
24
- role: MessageRole = Field(..., description="Role of the message sender (user or assistant)")
25
- content: str = Field(..., description="Content of the message")
26
- timestamp: datetime = Field(..., description="When the message was sent")
25
+ role: MessageRole = Field(..., description="Role of the message sender (user or assistant).")
26
+ content: str = Field(..., description="Content of the message.")
27
+ timestamp: datetime = Field(..., description="When the message was sent.")
27
28
 
28
29
  class Config:
29
30
  """Pydantic configuration."""
@@ -33,6 +34,27 @@ class Message(BaseModel):
33
34
  }
34
35
 
35
36
 
37
+ class SessionMessagesList(BaseModel):
38
+ """
39
+ Represents a paginated list of messages in a session.
40
+ """
41
+
42
+ messages: List[Message] = Field(..., description="List of messages in the page.")
43
+ total: int = Field(..., description="Total number of messages in the session.")
44
+ has_more: bool = Field(..., description="Whether there are more messages to fetch.")
45
+
46
+ class Config:
47
+ frozen = True
48
+
49
+ @classmethod
50
+ def from_api_response(cls, data: Dict[str, Any]) -> "SessionMessagesList":
51
+ return cls(
52
+ messages=[Message(**msg) for msg in data["messages"]],
53
+ total=data["total"],
54
+ has_more=data["has_more"],
55
+ )
56
+
57
+
36
58
  class SessionStatus(str, enum.Enum):
37
59
  """
38
60
  Status of a session.
@@ -46,16 +68,16 @@ class SessionModel(BaseModel):
46
68
  """
47
69
  Represents a conversation session.
48
70
  """
49
- session_id: uuid.UUID = Field(..., description="Unique identifier for the session")
50
- status: SessionStatus = Field(..., description="Current status of the session")
51
- created_at: datetime = Field(..., description="When the session was created")
71
+ session_id: str = Field(..., description="Unique identifier for the session.")
72
+ status: SessionStatus = Field(..., description="Current status of the session.")
73
+ created_at: datetime = Field(..., description="When the session was created.")
74
+ metadata: Dict[str, Any] = Field(default_factory=dict, description="Optional metadata for the session.")
52
75
 
53
76
  class Config:
54
77
  """Pydantic configuration."""
55
78
  frozen = True
56
79
  json_encoders = {
57
80
  datetime: lambda dt: dt.isoformat(),
58
- uuid.UUID: lambda id: str(id)
59
81
  }
60
82
 
61
83
  @classmethod
@@ -64,15 +86,20 @@ class SessionModel(BaseModel):
64
86
  Create a SessionModel instance from an API response.
65
87
 
66
88
  Args:
67
- data: API response data
89
+ data: API response data.
68
90
 
69
91
  Returns:
70
- A SessionModel instance
92
+ A SessionModel instance.
71
93
  """
94
+ if "session" in data:
95
+ session_data = data["session"]
96
+ else:
97
+ session_data = data
72
98
  return cls(
73
- session_id=data["session_id"],
74
- status=data.get("status", SessionStatus.PENDING),
75
- created_at=data.get("created_at", datetime.now()),
99
+ session_id=session_data["session_id"],
100
+ status=session_data["status"],
101
+ created_at=session_data["created_at"],
102
+ metadata=session_data["metadata"],
76
103
  )
77
104
 
78
105
 
@@ -80,38 +107,48 @@ class SessionList(BaseModel):
80
107
  """
81
108
  Represents a paginated list of sessions.
82
109
  """
83
- sessions: List[SessionModel] = Field(..., description="List of sessions")
84
- total: int = Field(..., description="Total number of sessions")
85
- has_more: bool = Field(..., description="Whether there are more sessions to fetch")
110
+ sessions: List["Session"] = Field(..., description="List of sessions.")
111
+ total: int = Field(..., description="Total number of sessions.")
112
+ has_more: bool = Field(..., description="Whether there are more sessions to fetch.")
86
113
 
87
114
  class Config:
88
115
  """Pydantic configuration."""
89
116
  frozen = True
117
+ arbitrary_types_allowed = True
90
118
 
91
119
  @classmethod
92
- def from_api_response(cls, data: Dict[str, Any]) -> "SessionList":
120
+ def from_api_response(cls, data: Dict[str, Any], user_id: str, http_client: HTTPClient) -> "SessionList":
93
121
  """
94
122
  Create a SessionList instance from an API response.
95
123
 
96
124
  Args:
97
- data: API response data
125
+ data: API response data.
98
126
 
99
127
  Returns:
100
- A SessionList instance
128
+ A SessionList instance.
101
129
  """
130
+ from ..session import Session
102
131
  return cls(
103
- sessions=[SessionModel.from_api_response(session) for session in data["sessions"]],
132
+ sessions=[
133
+ Session(http_client, user_id, SessionModel.from_api_response(session)) for session in data["sessions"]
134
+ ],
104
135
  total=data["total"],
105
136
  has_more=data["has_more"],
106
137
  )
107
138
 
139
+ class RecallStrategy(str, enum.Enum):
140
+ """
141
+ Type of recall strategy.
142
+ """
143
+ LOW_LATENCY = "low_latency"
144
+ BALANCED = "balanced"
145
+ DEEP = "deep"
108
146
 
109
147
  class Context(BaseModel):
110
148
  """
111
149
  Represents the context for a session.
112
150
  """
113
- memory_used: bool = Field(..., description="Whether memory was used to generate the context")
114
- context: str = Field(..., description="The context for the session")
151
+ context: str = Field(..., description="The context for the session.")
115
152
 
116
153
  class Config:
117
154
  """Pydantic configuration."""
@@ -123,12 +160,11 @@ class Context(BaseModel):
123
160
  Create a Context instance from an API response.
124
161
 
125
162
  Args:
126
- data: API response data
163
+ data: API response data.
127
164
 
128
165
  Returns:
129
- A Context instance
166
+ A Context instance.
130
167
  """
131
168
  return cls(
132
- memory_used=data["memory_used"],
133
169
  context=data["context"],
134
170
  )