agno 2.3.19__py3-none-any.whl → 2.3.20__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.
- agno/agent/agent.py +2466 -2048
- agno/db/dynamo/utils.py +26 -3
- agno/db/firestore/utils.py +25 -10
- agno/db/gcs_json/utils.py +14 -2
- agno/db/in_memory/utils.py +14 -2
- agno/db/json/utils.py +14 -2
- agno/db/mysql/utils.py +13 -3
- agno/db/postgres/utils.py +13 -3
- agno/db/redis/utils.py +26 -10
- agno/db/schemas/memory.py +15 -19
- agno/db/singlestore/utils.py +13 -3
- agno/db/sqlite/utils.py +15 -3
- agno/db/utils.py +22 -0
- agno/models/litellm/chat.py +6 -0
- agno/os/routers/knowledge/schemas.py +1 -1
- agno/os/routers/memory/schemas.py +14 -1
- agno/os/routers/metrics/schemas.py +1 -1
- agno/os/schema.py +11 -9
- agno/run/__init__.py +2 -4
- agno/run/cancel.py +65 -52
- agno/run/cancellation_management/__init__.py +9 -0
- agno/run/cancellation_management/base.py +78 -0
- agno/run/cancellation_management/in_memory_cancellation_manager.py +100 -0
- agno/run/cancellation_management/redis_cancellation_manager.py +236 -0
- agno/team/team.py +1217 -1136
- agno/utils/response.py +1 -13
- agno/vectordb/weaviate/__init__.py +1 -1
- agno/workflow/workflow.py +23 -16
- {agno-2.3.19.dist-info → agno-2.3.20.dist-info}/METADATA +3 -2
- {agno-2.3.19.dist-info → agno-2.3.20.dist-info}/RECORD +33 -29
- {agno-2.3.19.dist-info → agno-2.3.20.dist-info}/WHEEL +0 -0
- {agno-2.3.19.dist-info → agno-2.3.20.dist-info}/licenses/LICENSE +0 -0
- {agno-2.3.19.dist-info → agno-2.3.20.dist-info}/top_level.txt +0 -0
agno/db/dynamo/utils.py
CHANGED
|
@@ -8,6 +8,7 @@ from agno.db.base import SessionType
|
|
|
8
8
|
from agno.db.schemas.culture import CulturalKnowledge
|
|
9
9
|
from agno.db.schemas.evals import EvalRunRecord
|
|
10
10
|
from agno.db.schemas.knowledge import KnowledgeRow
|
|
11
|
+
from agno.db.utils import get_sort_value
|
|
11
12
|
from agno.session import Session
|
|
12
13
|
from agno.utils.log import log_debug, log_error, log_info
|
|
13
14
|
|
|
@@ -174,13 +175,35 @@ def apply_pagination(
|
|
|
174
175
|
def apply_sorting(
|
|
175
176
|
items: List[Dict[str, Any]], sort_by: Optional[str] = None, sort_order: Optional[str] = None
|
|
176
177
|
) -> List[Dict[str, Any]]:
|
|
177
|
-
"""Apply sorting to a list of items.
|
|
178
|
+
"""Apply sorting to a list of items.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
items: The list of dictionaries to sort
|
|
182
|
+
sort_by: The field to sort by (defaults to 'created_at')
|
|
183
|
+
sort_order: The sort order ('asc' or 'desc')
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
The sorted list
|
|
187
|
+
|
|
188
|
+
Note:
|
|
189
|
+
If sorting by "updated_at", will fallback to "created_at" in case of None.
|
|
190
|
+
"""
|
|
191
|
+
if not items:
|
|
192
|
+
return items
|
|
193
|
+
|
|
178
194
|
if sort_by is None:
|
|
179
195
|
sort_by = "created_at"
|
|
180
196
|
|
|
181
|
-
|
|
197
|
+
is_descending = sort_order == "desc"
|
|
198
|
+
|
|
199
|
+
# Sort using the helper function that handles updated_at -> created_at fallback
|
|
200
|
+
sorted_records = sorted(
|
|
201
|
+
items,
|
|
202
|
+
key=lambda x: (get_sort_value(x, sort_by) is None, get_sort_value(x, sort_by)),
|
|
203
|
+
reverse=is_descending,
|
|
204
|
+
)
|
|
182
205
|
|
|
183
|
-
return
|
|
206
|
+
return sorted_records
|
|
184
207
|
|
|
185
208
|
|
|
186
209
|
# -- Session utils --
|
agno/db/firestore/utils.py
CHANGED
|
@@ -8,6 +8,7 @@ from uuid import uuid4
|
|
|
8
8
|
|
|
9
9
|
from agno.db.firestore.schemas import get_collection_indexes
|
|
10
10
|
from agno.db.schemas.culture import CulturalKnowledge
|
|
11
|
+
from agno.db.utils import get_sort_value
|
|
11
12
|
from agno.utils.log import log_debug, log_error, log_info, log_warning
|
|
12
13
|
|
|
13
14
|
try:
|
|
@@ -126,19 +127,33 @@ def apply_pagination(query, limit: Optional[int] = None, page: Optional[int] = N
|
|
|
126
127
|
def apply_sorting_to_records(
|
|
127
128
|
records: List[Dict[str, Any]], sort_by: Optional[str] = None, sort_order: Optional[str] = None
|
|
128
129
|
) -> List[Dict[str, Any]]:
|
|
129
|
-
"""Apply sorting to in-memory records (for cases where Firestore query sorting isn't possible).
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
"""Apply sorting to in-memory records (for cases where Firestore query sorting isn't possible).
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
records: The list of dictionaries to sort
|
|
134
|
+
sort_by: The field to sort by
|
|
135
|
+
sort_order: The sort order ('asc' or 'desc')
|
|
132
136
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
137
|
+
Returns:
|
|
138
|
+
The sorted list
|
|
139
|
+
|
|
140
|
+
Note:
|
|
141
|
+
If sorting by "updated_at", will fallback to "created_at" in case of None.
|
|
142
|
+
"""
|
|
143
|
+
if sort_by is None or not records:
|
|
144
|
+
return records
|
|
138
145
|
|
|
139
146
|
try:
|
|
140
|
-
|
|
141
|
-
|
|
147
|
+
is_descending = sort_order == "desc"
|
|
148
|
+
|
|
149
|
+
# Sort using the helper function that handles updated_at -> created_at fallback
|
|
150
|
+
sorted_records = sorted(
|
|
151
|
+
records,
|
|
152
|
+
key=lambda x: (get_sort_value(x, sort_by) is None, get_sort_value(x, sort_by)),
|
|
153
|
+
reverse=is_descending,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
return sorted_records
|
|
142
157
|
except Exception as e:
|
|
143
158
|
log_warning(f"Error sorting Firestore records: {e}")
|
|
144
159
|
return records
|
agno/db/gcs_json/utils.py
CHANGED
|
@@ -6,6 +6,7 @@ from typing import Any, Dict, List, Optional
|
|
|
6
6
|
from uuid import uuid4
|
|
7
7
|
|
|
8
8
|
from agno.db.schemas.culture import CulturalKnowledge
|
|
9
|
+
from agno.db.utils import get_sort_value
|
|
9
10
|
from agno.utils.log import log_debug
|
|
10
11
|
|
|
11
12
|
|
|
@@ -21,6 +22,9 @@ def apply_sorting(
|
|
|
21
22
|
|
|
22
23
|
Returns:
|
|
23
24
|
The sorted list
|
|
25
|
+
|
|
26
|
+
Note:
|
|
27
|
+
If sorting by "updated_at", will fallback to "created_at" in case of None.
|
|
24
28
|
"""
|
|
25
29
|
if sort_by is None or not data:
|
|
26
30
|
return data
|
|
@@ -31,8 +35,16 @@ def apply_sorting(
|
|
|
31
35
|
return data
|
|
32
36
|
|
|
33
37
|
try:
|
|
34
|
-
|
|
35
|
-
|
|
38
|
+
is_descending = sort_order != "asc" if sort_order else True
|
|
39
|
+
|
|
40
|
+
# Sort using the helper function that handles updated_at -> created_at fallback
|
|
41
|
+
sorted_records = sorted(
|
|
42
|
+
data,
|
|
43
|
+
key=lambda x: (get_sort_value(x, sort_by) is None, get_sort_value(x, sort_by)),
|
|
44
|
+
reverse=is_descending,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return sorted_records
|
|
36
48
|
except Exception as e:
|
|
37
49
|
log_debug(f"Error sorting data by '{sort_by}': {e}")
|
|
38
50
|
return data
|
agno/db/in_memory/utils.py
CHANGED
|
@@ -6,6 +6,7 @@ from typing import Any, Dict, List, Optional
|
|
|
6
6
|
from uuid import uuid4
|
|
7
7
|
|
|
8
8
|
from agno.db.schemas.culture import CulturalKnowledge
|
|
9
|
+
from agno.db.utils import get_sort_value
|
|
9
10
|
from agno.utils.log import log_debug
|
|
10
11
|
|
|
11
12
|
|
|
@@ -21,6 +22,9 @@ def apply_sorting(
|
|
|
21
22
|
|
|
22
23
|
Returns:
|
|
23
24
|
The sorted list
|
|
25
|
+
|
|
26
|
+
Note:
|
|
27
|
+
If sorting by "updated_at", will fallback to "created_at" in case of None.
|
|
24
28
|
"""
|
|
25
29
|
if sort_by is None or not data:
|
|
26
30
|
return data
|
|
@@ -31,8 +35,16 @@ def apply_sorting(
|
|
|
31
35
|
return data
|
|
32
36
|
|
|
33
37
|
try:
|
|
34
|
-
|
|
35
|
-
|
|
38
|
+
is_descending = sort_order != "asc" if sort_order else True
|
|
39
|
+
|
|
40
|
+
# Sort using the helper function that handles updated_at -> created_at fallback
|
|
41
|
+
sorted_records = sorted(
|
|
42
|
+
data,
|
|
43
|
+
key=lambda x: (get_sort_value(x, sort_by) is None, get_sort_value(x, sort_by)),
|
|
44
|
+
reverse=is_descending,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return sorted_records
|
|
36
48
|
except Exception as e:
|
|
37
49
|
log_debug(f"Error sorting data by '{sort_by}': {e}")
|
|
38
50
|
return data
|
agno/db/json/utils.py
CHANGED
|
@@ -6,6 +6,7 @@ from typing import Any, Dict, List, Optional
|
|
|
6
6
|
from uuid import uuid4
|
|
7
7
|
|
|
8
8
|
from agno.db.schemas.culture import CulturalKnowledge
|
|
9
|
+
from agno.db.utils import get_sort_value
|
|
9
10
|
from agno.utils.log import log_debug
|
|
10
11
|
|
|
11
12
|
|
|
@@ -21,6 +22,9 @@ def apply_sorting(
|
|
|
21
22
|
|
|
22
23
|
Returns:
|
|
23
24
|
The sorted list
|
|
25
|
+
|
|
26
|
+
Note:
|
|
27
|
+
If sorting by "updated_at", will fallback to "created_at" in case of None.
|
|
24
28
|
"""
|
|
25
29
|
if sort_by is None or not data:
|
|
26
30
|
return data
|
|
@@ -31,8 +35,16 @@ def apply_sorting(
|
|
|
31
35
|
return data
|
|
32
36
|
|
|
33
37
|
try:
|
|
34
|
-
|
|
35
|
-
|
|
38
|
+
is_descending = sort_order != "asc" if sort_order else True
|
|
39
|
+
|
|
40
|
+
# Sort using the helper function that handles updated_at -> created_at fallback
|
|
41
|
+
sorted_records = sorted(
|
|
42
|
+
data,
|
|
43
|
+
key=lambda x: (get_sort_value(x, sort_by) is None, get_sort_value(x, sort_by)),
|
|
44
|
+
reverse=is_descending,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return sorted_records
|
|
36
48
|
except Exception as e:
|
|
37
49
|
log_debug(f"Error sorting data by '{sort_by}': {e}")
|
|
38
50
|
return data
|
agno/db/mysql/utils.py
CHANGED
|
@@ -10,7 +10,7 @@ from agno.db.schemas.culture import CulturalKnowledge
|
|
|
10
10
|
from agno.utils.log import log_debug, log_error, log_warning
|
|
11
11
|
|
|
12
12
|
try:
|
|
13
|
-
from sqlalchemy import Engine, Table
|
|
13
|
+
from sqlalchemy import Engine, Table, func
|
|
14
14
|
from sqlalchemy.dialects import mysql
|
|
15
15
|
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession
|
|
16
16
|
from sqlalchemy.inspection import inspect
|
|
@@ -32,6 +32,11 @@ def apply_sorting(stmt, table: Table, sort_by: Optional[str] = None, sort_order:
|
|
|
32
32
|
|
|
33
33
|
Returns:
|
|
34
34
|
The modified statement with sorting applied
|
|
35
|
+
|
|
36
|
+
Note:
|
|
37
|
+
For 'updated_at' sorting, uses COALESCE(updated_at, created_at) to fall back
|
|
38
|
+
to created_at when updated_at is NULL. This ensures pre-2.0 records (which may
|
|
39
|
+
have NULL updated_at) are sorted correctly by their creation time.
|
|
35
40
|
"""
|
|
36
41
|
if sort_by is None:
|
|
37
42
|
return stmt
|
|
@@ -40,8 +45,13 @@ def apply_sorting(stmt, table: Table, sort_by: Optional[str] = None, sort_order:
|
|
|
40
45
|
log_debug(f"Invalid sort field: '{sort_by}'. Will not apply any sorting.")
|
|
41
46
|
return stmt
|
|
42
47
|
|
|
43
|
-
#
|
|
44
|
-
|
|
48
|
+
# For updated_at, use COALESCE to fall back to created_at if updated_at is NULL
|
|
49
|
+
# This handles pre-2.0 records that may have NULL updated_at values
|
|
50
|
+
if sort_by == "updated_at" and hasattr(table.c, "created_at"):
|
|
51
|
+
sort_column = func.coalesce(table.c.updated_at, table.c.created_at)
|
|
52
|
+
else:
|
|
53
|
+
sort_column = getattr(table.c, sort_by)
|
|
54
|
+
|
|
45
55
|
if sort_order and sort_order == "asc":
|
|
46
56
|
return stmt.order_by(sort_column.asc())
|
|
47
57
|
else:
|
agno/db/postgres/utils.py
CHANGED
|
@@ -13,7 +13,7 @@ from agno.db.schemas.culture import CulturalKnowledge
|
|
|
13
13
|
from agno.utils.log import log_debug, log_error, log_warning
|
|
14
14
|
|
|
15
15
|
try:
|
|
16
|
-
from sqlalchemy import Table
|
|
16
|
+
from sqlalchemy import Table, func
|
|
17
17
|
from sqlalchemy.dialects import postgresql
|
|
18
18
|
from sqlalchemy.inspection import inspect
|
|
19
19
|
from sqlalchemy.orm import Session
|
|
@@ -34,6 +34,11 @@ def apply_sorting(stmt, table: Table, sort_by: Optional[str] = None, sort_order:
|
|
|
34
34
|
|
|
35
35
|
Returns:
|
|
36
36
|
The modified statement with sorting applied
|
|
37
|
+
|
|
38
|
+
Note:
|
|
39
|
+
For 'updated_at' sorting, uses COALESCE(updated_at, created_at) to fall back
|
|
40
|
+
to created_at when updated_at is NULL. This ensures pre-2.0 records (which may
|
|
41
|
+
have NULL updated_at) are sorted correctly by their creation time.
|
|
37
42
|
"""
|
|
38
43
|
if sort_by is None:
|
|
39
44
|
return stmt
|
|
@@ -42,8 +47,13 @@ def apply_sorting(stmt, table: Table, sort_by: Optional[str] = None, sort_order:
|
|
|
42
47
|
log_debug(f"Invalid sort field: '{sort_by}'. Will not apply any sorting.")
|
|
43
48
|
return stmt
|
|
44
49
|
|
|
45
|
-
#
|
|
46
|
-
|
|
50
|
+
# For updated_at, use COALESCE to fall back to created_at if updated_at is NULL
|
|
51
|
+
# This handles pre-2.0 records that may have NULL updated_at values
|
|
52
|
+
if sort_by == "updated_at" and hasattr(table.c, "created_at"):
|
|
53
|
+
sort_column = func.coalesce(table.c.updated_at, table.c.created_at)
|
|
54
|
+
else:
|
|
55
|
+
sort_column = getattr(table.c, sort_by)
|
|
56
|
+
|
|
47
57
|
if sort_order and sort_order == "asc":
|
|
48
58
|
return stmt.order_by(sort_column.asc())
|
|
49
59
|
else:
|
agno/db/redis/utils.py
CHANGED
|
@@ -7,6 +7,7 @@ from typing import Any, Dict, List, Optional, Union
|
|
|
7
7
|
from uuid import UUID
|
|
8
8
|
|
|
9
9
|
from agno.db.schemas.culture import CulturalKnowledge
|
|
10
|
+
from agno.db.utils import get_sort_value
|
|
10
11
|
from agno.utils.log import log_warning
|
|
11
12
|
|
|
12
13
|
try:
|
|
@@ -80,21 +81,36 @@ def get_all_keys_for_table(redis_client: Union[Redis, RedisCluster], prefix: str
|
|
|
80
81
|
def apply_sorting(
|
|
81
82
|
records: List[Dict[str, Any]], sort_by: Optional[str] = None, sort_order: Optional[str] = None
|
|
82
83
|
) -> List[Dict[str, Any]]:
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
"""Apply sorting to the given records list.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
records: The list of dictionaries to sort
|
|
88
|
+
sort_by: The field to sort by
|
|
89
|
+
sort_order: The sort order ('asc' or 'desc')
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
The sorted list
|
|
85
93
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return
|
|
94
|
+
Note:
|
|
95
|
+
If sorting by "updated_at", will fallback to "created_at" in case of None.
|
|
96
|
+
"""
|
|
97
|
+
if sort_by is None or not records:
|
|
98
|
+
return records
|
|
91
99
|
|
|
92
100
|
try:
|
|
93
|
-
|
|
94
|
-
|
|
101
|
+
is_descending = sort_order == "desc"
|
|
102
|
+
|
|
103
|
+
# Sort using the helper function that handles updated_at -> created_at fallback
|
|
104
|
+
sorted_records = sorted(
|
|
105
|
+
records,
|
|
106
|
+
key=lambda x: (get_sort_value(x, sort_by) is None, get_sort_value(x, sort_by)),
|
|
107
|
+
reverse=is_descending,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return sorted_records
|
|
95
111
|
|
|
96
112
|
except Exception as e:
|
|
97
|
-
log_warning(f"Error sorting
|
|
113
|
+
log_warning(f"Error sorting Redis records: {e}")
|
|
98
114
|
return records
|
|
99
115
|
|
|
100
116
|
|
agno/db/schemas/memory.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
|
-
from datetime import datetime
|
|
2
|
+
from datetime import datetime
|
|
3
3
|
from typing import Any, Dict, List, Optional
|
|
4
4
|
|
|
5
|
-
from agno.utils.dttm import now_epoch_s
|
|
5
|
+
from agno.utils.dttm import now_epoch_s, to_epoch_s
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
@dataclass
|
|
@@ -22,17 +22,20 @@ class UserMemory:
|
|
|
22
22
|
team_id: Optional[str] = None
|
|
23
23
|
|
|
24
24
|
def __post_init__(self) -> None:
|
|
25
|
-
"""Automatically set created_at
|
|
26
|
-
if self.created_at is None
|
|
27
|
-
|
|
25
|
+
"""Automatically set/normalize created_at and updated_at."""
|
|
26
|
+
self.created_at = now_epoch_s() if self.created_at is None else to_epoch_s(self.created_at)
|
|
27
|
+
if self.updated_at is not None:
|
|
28
|
+
self.updated_at = to_epoch_s(self.updated_at)
|
|
28
29
|
|
|
29
30
|
def to_dict(self) -> Dict[str, Any]:
|
|
31
|
+
created_at = datetime.fromtimestamp(self.created_at).isoformat() if self.created_at is not None else None
|
|
32
|
+
updated_at = datetime.fromtimestamp(self.updated_at).isoformat() if self.updated_at is not None else created_at
|
|
30
33
|
_dict = {
|
|
31
34
|
"memory_id": self.memory_id,
|
|
32
35
|
"memory": self.memory,
|
|
33
36
|
"topics": self.topics,
|
|
34
|
-
"created_at":
|
|
35
|
-
"updated_at":
|
|
37
|
+
"created_at": created_at,
|
|
38
|
+
"updated_at": updated_at,
|
|
36
39
|
"input": self.input,
|
|
37
40
|
"user_id": self.user_id,
|
|
38
41
|
"agent_id": self.agent_id,
|
|
@@ -45,17 +48,10 @@ class UserMemory:
|
|
|
45
48
|
def from_dict(cls, data: Dict[str, Any]) -> "UserMemory":
|
|
46
49
|
data = dict(data)
|
|
47
50
|
|
|
48
|
-
if
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
# Convert updated_at to datetime
|
|
55
|
-
if updated_at := data.get("updated_at"):
|
|
56
|
-
if isinstance(updated_at, (int, float)):
|
|
57
|
-
data["updated_at"] = datetime.fromtimestamp(updated_at, tz=timezone.utc)
|
|
58
|
-
else:
|
|
59
|
-
data["updated_at"] = datetime.fromisoformat(updated_at)
|
|
51
|
+
# Preserve 0 and None explicitly; only process if key exists
|
|
52
|
+
if "created_at" in data and data["created_at"] is not None:
|
|
53
|
+
data["created_at"] = to_epoch_s(data["created_at"])
|
|
54
|
+
if "updated_at" in data and data["updated_at"] is not None:
|
|
55
|
+
data["updated_at"] = to_epoch_s(data["updated_at"])
|
|
60
56
|
|
|
61
57
|
return cls(**data)
|
agno/db/singlestore/utils.py
CHANGED
|
@@ -12,7 +12,7 @@ from agno.db.singlestore.schemas import get_table_schema_definition
|
|
|
12
12
|
from agno.utils.log import log_debug, log_error, log_warning
|
|
13
13
|
|
|
14
14
|
try:
|
|
15
|
-
from sqlalchemy import Table
|
|
15
|
+
from sqlalchemy import Table, func
|
|
16
16
|
from sqlalchemy.inspection import inspect
|
|
17
17
|
from sqlalchemy.orm import Session
|
|
18
18
|
from sqlalchemy.sql.expression import text
|
|
@@ -32,6 +32,11 @@ def apply_sorting(stmt, table: Table, sort_by: Optional[str] = None, sort_order:
|
|
|
32
32
|
|
|
33
33
|
Returns:
|
|
34
34
|
The modified statement with sorting applied
|
|
35
|
+
|
|
36
|
+
Note:
|
|
37
|
+
For 'updated_at' sorting, uses COALESCE(updated_at, created_at) to fall back
|
|
38
|
+
to created_at when updated_at is NULL. This ensures pre-2.0 records (which may
|
|
39
|
+
have NULL updated_at) are sorted correctly by their creation time.
|
|
35
40
|
"""
|
|
36
41
|
if sort_by is None:
|
|
37
42
|
return stmt
|
|
@@ -40,8 +45,13 @@ def apply_sorting(stmt, table: Table, sort_by: Optional[str] = None, sort_order:
|
|
|
40
45
|
log_debug(f"Invalid sort field: '{sort_by}'. Will not apply any sorting.")
|
|
41
46
|
return stmt
|
|
42
47
|
|
|
43
|
-
#
|
|
44
|
-
|
|
48
|
+
# For updated_at, use COALESCE to fall back to created_at if updated_at is NULL
|
|
49
|
+
# This handles pre-2.0 records that may have NULL updated_at values
|
|
50
|
+
if sort_by == "updated_at" and hasattr(table.c, "created_at"):
|
|
51
|
+
sort_column = func.coalesce(table.c.updated_at, table.c.created_at)
|
|
52
|
+
else:
|
|
53
|
+
sort_column = getattr(table.c, sort_by)
|
|
54
|
+
|
|
45
55
|
if sort_order and sort_order == "asc":
|
|
46
56
|
return stmt.order_by(sort_column.asc())
|
|
47
57
|
else:
|
agno/db/sqlite/utils.py
CHANGED
|
@@ -11,7 +11,7 @@ from agno.db.sqlite.schemas import get_table_schema_definition
|
|
|
11
11
|
from agno.utils.log import log_debug, log_error, log_warning
|
|
12
12
|
|
|
13
13
|
try:
|
|
14
|
-
from sqlalchemy import Table
|
|
14
|
+
from sqlalchemy import Table, func
|
|
15
15
|
from sqlalchemy.dialects import sqlite
|
|
16
16
|
from sqlalchemy.engine import Engine
|
|
17
17
|
from sqlalchemy.inspection import inspect
|
|
@@ -26,13 +26,20 @@ except ImportError:
|
|
|
26
26
|
|
|
27
27
|
def apply_sorting(stmt, table: Table, sort_by: Optional[str] = None, sort_order: Optional[str] = None):
|
|
28
28
|
"""Apply sorting to the given SQLAlchemy statement.
|
|
29
|
+
|
|
29
30
|
Args:
|
|
30
31
|
stmt: The SQLAlchemy statement to modify
|
|
31
32
|
table: The table being queried
|
|
32
33
|
sort_by: The field to sort by
|
|
33
34
|
sort_order: The sort order ('asc' or 'desc')
|
|
35
|
+
|
|
34
36
|
Returns:
|
|
35
37
|
The modified statement with sorting applied
|
|
38
|
+
|
|
39
|
+
Note:
|
|
40
|
+
For 'updated_at' sorting, uses COALESCE(updated_at, created_at) to fall back
|
|
41
|
+
to created_at when updated_at is NULL. This ensures pre-2.0 records (which may
|
|
42
|
+
have NULL updated_at) are sorted correctly by their creation time.
|
|
36
43
|
"""
|
|
37
44
|
if sort_by is None:
|
|
38
45
|
return stmt
|
|
@@ -40,8 +47,13 @@ def apply_sorting(stmt, table: Table, sort_by: Optional[str] = None, sort_order:
|
|
|
40
47
|
log_debug(f"Invalid sort field: '{sort_by}'. Will not apply any sorting.")
|
|
41
48
|
return stmt
|
|
42
49
|
|
|
43
|
-
#
|
|
44
|
-
|
|
50
|
+
# For updated_at, use COALESCE to fall back to created_at if updated_at is NULL
|
|
51
|
+
# This handles pre-2.0 records that may have NULL updated_at values
|
|
52
|
+
if sort_by == "updated_at" and hasattr(table.c, "created_at"):
|
|
53
|
+
sort_column = func.coalesce(table.c.updated_at, table.c.created_at)
|
|
54
|
+
else:
|
|
55
|
+
sort_column = getattr(table.c, sort_by)
|
|
56
|
+
|
|
45
57
|
if sort_order and sort_order == "asc":
|
|
46
58
|
return stmt.order_by(sort_column.asc())
|
|
47
59
|
else:
|
agno/db/utils.py
CHANGED
|
@@ -2,12 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
from datetime import date, datetime
|
|
5
|
+
from typing import Any, Dict
|
|
5
6
|
from uuid import UUID
|
|
6
7
|
|
|
7
8
|
from agno.models.message import Message
|
|
8
9
|
from agno.models.metrics import Metrics
|
|
9
10
|
|
|
10
11
|
|
|
12
|
+
def get_sort_value(record: Dict[str, Any], sort_by: str) -> Any:
|
|
13
|
+
"""Get the sort value for a record, with fallback to created_at for updated_at.
|
|
14
|
+
|
|
15
|
+
When sorting by 'updated_at', this function falls back to 'created_at' if
|
|
16
|
+
'updated_at' is None. This ensures pre-2.0 records (which may have NULL
|
|
17
|
+
updated_at values) are sorted correctly by their creation time.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
record: The record dictionary to get the sort value from
|
|
21
|
+
sort_by: The field to sort by
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
The value to use for sorting
|
|
25
|
+
"""
|
|
26
|
+
value = record.get(sort_by)
|
|
27
|
+
# For updated_at, fall back to created_at if updated_at is None
|
|
28
|
+
if value is None and sort_by == "updated_at":
|
|
29
|
+
value = record.get("created_at")
|
|
30
|
+
return value
|
|
31
|
+
|
|
32
|
+
|
|
11
33
|
class CustomJSONEncoder(json.JSONEncoder):
|
|
12
34
|
"""Custom encoder to handle non JSON serializable types."""
|
|
13
35
|
|
agno/models/litellm/chat.py
CHANGED
|
@@ -307,6 +307,9 @@ class LiteLLM(Model):
|
|
|
307
307
|
if response_message.content is not None:
|
|
308
308
|
model_response.content = response_message.content
|
|
309
309
|
|
|
310
|
+
if hasattr(response_message, "reasoning_content") and response_message.reasoning_content is not None:
|
|
311
|
+
model_response.reasoning_content = response_message.reasoning_content
|
|
312
|
+
|
|
310
313
|
if hasattr(response_message, "tool_calls") and response_message.tool_calls:
|
|
311
314
|
model_response.tool_calls = []
|
|
312
315
|
for tool_call in response_message.tool_calls:
|
|
@@ -334,6 +337,9 @@ class LiteLLM(Model):
|
|
|
334
337
|
if hasattr(choice_delta, "content") and choice_delta.content is not None:
|
|
335
338
|
model_response.content = choice_delta.content
|
|
336
339
|
|
|
340
|
+
if hasattr(choice_delta, "reasoning_content") and choice_delta.reasoning_content is not None:
|
|
341
|
+
model_response.reasoning_content = choice_delta.reasoning_content
|
|
342
|
+
|
|
337
343
|
if hasattr(choice_delta, "tool_calls") and choice_delta.tool_calls:
|
|
338
344
|
processed_tool_calls = []
|
|
339
345
|
for tool_call in choice_delta.tool_calls:
|
|
@@ -82,7 +82,7 @@ class ContentResponseSchema(BaseModel):
|
|
|
82
82
|
status=status,
|
|
83
83
|
status_message=content.get("status_message"),
|
|
84
84
|
created_at=parse_timestamp(content.get("created_at")),
|
|
85
|
-
updated_at=parse_timestamp(content.get("updated_at")),
|
|
85
|
+
updated_at=parse_timestamp(content.get("updated_at", content.get("created_at", 0))),
|
|
86
86
|
# TODO: These fields are not available in the Content class. Fix the inconsistency
|
|
87
87
|
access_count=None,
|
|
88
88
|
linked_to=None,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import json
|
|
1
2
|
from datetime import datetime, timezone
|
|
2
3
|
from typing import Any, Dict, List, Optional
|
|
3
4
|
|
|
@@ -25,12 +26,24 @@ class UserMemorySchema(BaseModel):
|
|
|
25
26
|
if memory_dict["memory"] == "":
|
|
26
27
|
return None
|
|
27
28
|
|
|
29
|
+
# Handle nested memory content (relevant for some memories migrated from v1)
|
|
30
|
+
if isinstance(memory_dict["memory"], dict):
|
|
31
|
+
if memory_dict["memory"].get("memory") is not None:
|
|
32
|
+
memory = str(memory_dict["memory"]["memory"])
|
|
33
|
+
else:
|
|
34
|
+
try:
|
|
35
|
+
memory = json.dumps(memory_dict["memory"])
|
|
36
|
+
except json.JSONDecodeError:
|
|
37
|
+
memory = str(memory_dict["memory"])
|
|
38
|
+
else:
|
|
39
|
+
memory = memory_dict["memory"]
|
|
40
|
+
|
|
28
41
|
return cls(
|
|
29
42
|
memory_id=memory_dict["memory_id"],
|
|
30
43
|
user_id=str(memory_dict["user_id"]),
|
|
31
44
|
agent_id=memory_dict.get("agent_id"),
|
|
32
45
|
team_id=memory_dict.get("team_id"),
|
|
33
|
-
memory=
|
|
46
|
+
memory=memory,
|
|
34
47
|
topics=memory_dict.get("topics", []),
|
|
35
48
|
updated_at=memory_dict["updated_at"],
|
|
36
49
|
)
|
|
@@ -35,7 +35,7 @@ class DayAggregatedMetrics(BaseModel):
|
|
|
35
35
|
team_runs_count=metrics_dict.get("team_runs_count", 0),
|
|
36
36
|
team_sessions_count=metrics_dict.get("team_sessions_count", 0),
|
|
37
37
|
token_metrics=metrics_dict.get("token_metrics", {}),
|
|
38
|
-
updated_at=metrics_dict.get("updated_at", 0),
|
|
38
|
+
updated_at=metrics_dict.get("updated_at", metrics_dict.get("created_at", 0)),
|
|
39
39
|
users_count=metrics_dict.get("users_count", 0),
|
|
40
40
|
workflow_runs_count=metrics_dict.get("workflow_runs_count", 0),
|
|
41
41
|
workflow_sessions_count=metrics_dict.get("workflow_sessions_count", 0),
|
agno/os/schema.py
CHANGED
|
@@ -193,7 +193,7 @@ class SessionSchema(BaseModel):
|
|
|
193
193
|
session_data = session.get("session_data", {}) or {}
|
|
194
194
|
|
|
195
195
|
created_at = session.get("created_at", 0)
|
|
196
|
-
updated_at = session.get("updated_at",
|
|
196
|
+
updated_at = session.get("updated_at", created_at)
|
|
197
197
|
|
|
198
198
|
# Handle created_at and updated_at as either ISO 8601 string or timestamp
|
|
199
199
|
def parse_datetime(val):
|
|
@@ -213,7 +213,7 @@ class SessionSchema(BaseModel):
|
|
|
213
213
|
return None
|
|
214
214
|
|
|
215
215
|
created_at = parse_datetime(session.get("created_at", 0))
|
|
216
|
-
updated_at = parse_datetime(session.get("updated_at",
|
|
216
|
+
updated_at = parse_datetime(session.get("updated_at", created_at))
|
|
217
217
|
return cls(
|
|
218
218
|
session_id=session.get("session_id", ""),
|
|
219
219
|
session_name=session_name,
|
|
@@ -265,6 +265,8 @@ class AgentSessionDetailSchema(BaseModel):
|
|
|
265
265
|
@classmethod
|
|
266
266
|
def from_session(cls, session: AgentSession) -> "AgentSessionDetailSchema":
|
|
267
267
|
session_name = get_session_name({**session.to_dict(), "session_type": "agent"})
|
|
268
|
+
created_at = datetime.fromtimestamp(session.created_at, tz=timezone.utc) if session.created_at else None
|
|
269
|
+
updated_at = datetime.fromtimestamp(session.updated_at, tz=timezone.utc) if session.updated_at else created_at
|
|
268
270
|
return cls(
|
|
269
271
|
user_id=session.user_id,
|
|
270
272
|
agent_session_id=session.session_id,
|
|
@@ -280,8 +282,8 @@ class AgentSessionDetailSchema(BaseModel):
|
|
|
280
282
|
metrics=session.session_data.get("session_metrics", {}) if session.session_data else None, # type: ignore
|
|
281
283
|
metadata=session.metadata,
|
|
282
284
|
chat_history=[message.to_dict() for message in session.get_chat_history()],
|
|
283
|
-
created_at=
|
|
284
|
-
updated_at=
|
|
285
|
+
created_at=created_at,
|
|
286
|
+
updated_at=updated_at,
|
|
285
287
|
)
|
|
286
288
|
|
|
287
289
|
|
|
@@ -304,7 +306,8 @@ class TeamSessionDetailSchema(BaseModel):
|
|
|
304
306
|
def from_session(cls, session: TeamSession) -> "TeamSessionDetailSchema":
|
|
305
307
|
session_dict = session.to_dict()
|
|
306
308
|
session_name = get_session_name({**session_dict, "session_type": "team"})
|
|
307
|
-
|
|
309
|
+
created_at = datetime.fromtimestamp(session.created_at, tz=timezone.utc) if session.created_at else None
|
|
310
|
+
updated_at = datetime.fromtimestamp(session.updated_at, tz=timezone.utc) if session.updated_at else created_at
|
|
308
311
|
return cls(
|
|
309
312
|
session_id=session.session_id,
|
|
310
313
|
team_id=session.team_id,
|
|
@@ -319,8 +322,8 @@ class TeamSessionDetailSchema(BaseModel):
|
|
|
319
322
|
metrics=session.session_data.get("session_metrics", {}) if session.session_data else None,
|
|
320
323
|
metadata=session.metadata,
|
|
321
324
|
chat_history=[message.to_dict() for message in session.get_chat_history()],
|
|
322
|
-
created_at=
|
|
323
|
-
updated_at=
|
|
325
|
+
created_at=created_at,
|
|
326
|
+
updated_at=updated_at,
|
|
324
327
|
)
|
|
325
328
|
|
|
326
329
|
|
|
@@ -343,7 +346,6 @@ class WorkflowSessionDetailSchema(BaseModel):
|
|
|
343
346
|
def from_session(cls, session: WorkflowSession) -> "WorkflowSessionDetailSchema":
|
|
344
347
|
session_dict = session.to_dict()
|
|
345
348
|
session_name = get_session_name({**session_dict, "session_type": "workflow"})
|
|
346
|
-
|
|
347
349
|
return cls(
|
|
348
350
|
session_id=session.session_id,
|
|
349
351
|
user_id=session.user_id,
|
|
@@ -355,7 +357,7 @@ class WorkflowSessionDetailSchema(BaseModel):
|
|
|
355
357
|
workflow_data=session.workflow_data,
|
|
356
358
|
metadata=session.metadata,
|
|
357
359
|
created_at=session.created_at,
|
|
358
|
-
updated_at=session.updated_at,
|
|
360
|
+
updated_at=session.updated_at or session.created_at,
|
|
359
361
|
)
|
|
360
362
|
|
|
361
363
|
|