zrb 1.0.0b2__py3-none-any.whl → 1.0.0b4__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.
Files changed (45) hide show
  1. zrb/__main__.py +3 -0
  2. zrb/builtin/llm/llm_chat.py +85 -5
  3. zrb/builtin/llm/previous-session.js +13 -0
  4. zrb/builtin/llm/tool/api.py +29 -0
  5. zrb/builtin/llm/tool/cli.py +1 -1
  6. zrb/builtin/llm/tool/rag.py +108 -145
  7. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/client_method.py +6 -6
  8. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/gateway_subroute.py +3 -1
  9. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/base_db_repository.py +88 -44
  10. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/config.py +12 -0
  11. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/client/auth_client.py +28 -22
  12. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/migration/versions/3093c7336477_add_auth_tables.py +6 -6
  13. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/repository/role_db_repository.py +43 -29
  14. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/repository/role_repository.py +8 -0
  15. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/role_service.py +46 -14
  16. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/repository/user_db_repository.py +158 -20
  17. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/repository/user_repository.py +29 -0
  18. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/user_service.py +36 -14
  19. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/subroute/auth.py +14 -14
  20. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/permission.py +1 -1
  21. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/role.py +34 -6
  22. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/session.py +2 -6
  23. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/user.py +41 -2
  24. zrb/builtin/todo.py +1 -0
  25. zrb/config.py +23 -4
  26. zrb/input/any_input.py +5 -0
  27. zrb/input/base_input.py +6 -0
  28. zrb/input/bool_input.py +2 -0
  29. zrb/input/float_input.py +2 -0
  30. zrb/input/int_input.py +2 -0
  31. zrb/input/option_input.py +2 -0
  32. zrb/input/password_input.py +2 -0
  33. zrb/input/text_input.py +2 -0
  34. zrb/runner/common_util.py +1 -1
  35. zrb/runner/web_route/error_page/show_error_page.py +2 -1
  36. zrb/runner/web_route/static/resources/session/current-session.js +4 -2
  37. zrb/runner/web_route/static/resources/session/event.js +8 -2
  38. zrb/runner/web_route/task_session_api_route.py +48 -3
  39. zrb/task/base_task.py +14 -13
  40. zrb/task/llm_task.py +214 -84
  41. zrb/util/llm/tool.py +3 -7
  42. {zrb-1.0.0b2.dist-info → zrb-1.0.0b4.dist-info}/METADATA +2 -1
  43. {zrb-1.0.0b2.dist-info → zrb-1.0.0b4.dist-info}/RECORD +45 -43
  44. {zrb-1.0.0b2.dist-info → zrb-1.0.0b4.dist-info}/WHEEL +0 -0
  45. {zrb-1.0.0b2.dist-info → zrb-1.0.0b4.dist-info}/entry_points.txt +0 -0
@@ -1,11 +1,23 @@
1
- from typing import Any
1
+ import datetime
2
+ from typing import Any, Callable
2
3
 
4
+ import ulid
3
5
  from my_app_name.common.base_db_repository import BaseDBRepository
6
+ from my_app_name.common.error import NotFoundError
7
+ from my_app_name.config import (
8
+ APP_AUTH_GUEST_USER,
9
+ APP_AUTH_GUEST_USER_PERMISSIONS,
10
+ APP_AUTH_SUPER_USER,
11
+ APP_AUTH_SUPER_USER_PASSWORD,
12
+ APP_MAX_PARALLEL_SESSION,
13
+ APP_SESSION_EXPIRE_MINUTES,
14
+ )
4
15
  from my_app_name.module.auth.service.user.repository.user_repository import (
5
16
  UserRepository,
6
17
  )
7
18
  from my_app_name.schema.permission import Permission
8
19
  from my_app_name.schema.role import Role, RolePermission
20
+ from my_app_name.schema.session import Session, SessionResponse
9
21
  from my_app_name.schema.user import (
10
22
  User,
11
23
  UserCreateWithAudit,
@@ -14,8 +26,10 @@ from my_app_name.schema.user import (
14
26
  UserUpdateWithAudit,
15
27
  )
16
28
  from passlib.context import CryptContext
17
- from sqlalchemy.sql import Select
18
- from sqlmodel import select
29
+ from sqlalchemy.engine import Engine
30
+ from sqlalchemy.ext.asyncio import AsyncEngine
31
+ from sqlalchemy.sql import ClauseElement, ColumnElement, Select
32
+ from sqlmodel import SQLModel, delete, insert, select
19
33
 
20
34
  # Password hashing context
21
35
  pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
@@ -36,39 +50,163 @@ class UserDBRepository(
36
50
  entity_name = "user"
37
51
  column_preprocessors = {"password": hash_password}
38
52
 
53
+ def __init__(
54
+ self,
55
+ engine: Engine | AsyncEngine,
56
+ super_user_username: str = APP_AUTH_SUPER_USER,
57
+ super_user_password: str = APP_AUTH_SUPER_USER_PASSWORD,
58
+ guest_user_username: str = APP_AUTH_GUEST_USER,
59
+ guest_user_password: str = APP_AUTH_SUPER_USER_PASSWORD,
60
+ guest_user_permission_names: list[str] = APP_AUTH_GUEST_USER_PERMISSIONS,
61
+ max_parallel_session: int = APP_MAX_PARALLEL_SESSION,
62
+ session_expire_minutes: int = APP_SESSION_EXPIRE_MINUTES,
63
+ filter_param_parser: (
64
+ Callable[[SQLModel, str], list[ClauseElement]] | None
65
+ ) = None,
66
+ sort_param_parser: Callable[[SQLModel, str], list[ColumnElement]] | None = None,
67
+ ):
68
+ super().__init__(
69
+ engine=engine,
70
+ filter_param_parser=filter_param_parser,
71
+ sort_param_parser=sort_param_parser,
72
+ )
73
+ self._super_user_username = super_user_username
74
+ self._super_user_passwored = super_user_password
75
+ self._guest_user_username = guest_user_username
76
+ self._guest_user_password = guest_user_password
77
+ self._guest_user_permission_names = guest_user_permission_names
78
+ self._max_parallel_session = max_parallel_session
79
+ self._session_expire_minutes = session_expire_minutes
80
+ self._super_user: User | None = None
81
+ self._guest_user: User | None = None
82
+
39
83
  def _select(self) -> Select:
40
84
  return (
41
- select(User, Role, Permission)
85
+ select(User, Role, Permission, Session)
42
86
  .join(UserRole, UserRole.user_id == User.id, isouter=True)
43
87
  .join(Role, Role.id == UserRole.role_id, isouter=True)
44
88
  .join(RolePermission, RolePermission.role_id == Role.id, isouter=True)
45
- .join(Permission, Permission.id == RolePermission.role_id, isouter=True)
89
+ .join(
90
+ Permission, Permission.id == RolePermission.permission_id, isouter=True
91
+ )
92
+ .join(Session, Session.user_id == User.id)
46
93
  )
47
94
 
48
- def _rows_to_responses(
49
- self, rows: list[tuple[User, Role, Permission]]
50
- ) -> UserResponse:
95
+ def _rows_to_responses(self, rows: list[tuple[Any, ...]]) -> list[UserResponse]:
51
96
  user_map: dict[str, dict[str, Any]] = {}
52
- for user, role, permission in rows:
97
+ user_role_map: dict[str, list[str]] = {}
98
+ user_permission_map: dict[str, list[str]] = {}
99
+ for user, role, permission, _ in rows:
53
100
  if user.id not in user_map:
54
- user_map[user.id] = {"user": user, "roles": set(), "permissions": set()}
55
- if role:
56
- user_map[user.id]["roles"].add(role)
57
- if permission:
58
- user_map[user.id]["permissions"].add(permission)
101
+ user_map[user.id] = {"user": user, "roles": [], "permissions": []}
102
+ user_role_map[user.id] = []
103
+ user_permission_map[user.id] = []
104
+ if role is not None and role.id not in user_role_map[user.id]:
105
+ user_role_map[user.id].append(role.id)
106
+ user_map[user.id]["roles"].append(role.model_dump())
107
+ if (
108
+ permission is not None
109
+ and permission.id not in user_permission_map[user.id]
110
+ ):
111
+ user_permission_map[user.id].append(permission.id)
112
+ user_map[user.id]["permissions"].append(permission.model_dump())
59
113
  return [
60
114
  UserResponse(
61
115
  **data["user"].model_dump(),
62
116
  roles=list(data["roles"]),
63
- permissions=list(data["permissions"])
117
+ permissions=list(data["permissions"]),
64
118
  )
65
119
  for data in user_map.values()
66
120
  ]
67
121
 
122
+ async def add_roles(self, data: dict[str, list[str]], created_by: str):
123
+ now = datetime.datetime.now(datetime.timezone.utc)
124
+ data_dict_list: list[dict[str, Any]] = []
125
+ for user_id, role_ids in data.items():
126
+ for role_id in role_ids:
127
+ data_dict_list.append(
128
+ self._model_to_data_dict(
129
+ UserRole(
130
+ id=ulid.new().str,
131
+ user_id=user_id,
132
+ role_id=role_id,
133
+ created_at=now,
134
+ created_by=created_by,
135
+ )
136
+ )
137
+ )
138
+ async with self._session_scope() as session:
139
+ await self._execute_statement(
140
+ session, insert(UserRole).values(data_dict_list)
141
+ )
142
+
143
+ async def remove_all_roles(self, user_ids: list[str] = []):
144
+ async with self._session_scope() as session:
145
+ await self._execute_statement(
146
+ session,
147
+ delete(UserRole).where(UserRole.user_id._in(user_ids)),
148
+ )
149
+
68
150
  async def get_by_credentials(self, username: str, password: str) -> UserResponse:
69
- select_statement = self._select().where(
70
- User.username == username, User.password == hash_password(password)
151
+ rows = await self._select_to_response(
152
+ lambda q: q.where(
153
+ User.username == username, User.password == hash_password(password)
154
+ )
71
155
  )
72
- rows = await self._execute_select_statement(select_statement)
73
- responses = self._rows_to_responses(rows)
74
- return self._ensure_one(responses)
156
+ return self._ensure_one(rows)
157
+
158
+ async def get_by_token(self, token: str) -> UserResponse:
159
+ rows = await self._select_tor_response(
160
+ lambda q: q.where(Session.token == token)
161
+ )
162
+ return self._ensure_one(rows)
163
+
164
+ async def add_token(self, user_id: str, token: str):
165
+ async with self._session_scope() as session:
166
+ await self._execute_statement(
167
+ session,
168
+ insert(Session).values(
169
+ {
170
+ "id": ulid.new().str,
171
+ "user_id": user_id,
172
+ "token": token,
173
+ "created_by": "system",
174
+ "created_at": datetime.datetime.now(datetime.timezone.utc),
175
+ }
176
+ ),
177
+ )
178
+
179
+ async def remove_token(self, user_id: str, token: str):
180
+ async with self._session_scope() as session:
181
+ await self._execute_statement(
182
+ session,
183
+ delete(Session).where(
184
+ Session.token == token, Session.user_id == user_id
185
+ ),
186
+ )
187
+
188
+ async def get_sessions(self, user_id: str) -> list[SessionResponse]:
189
+ async with self._session_scope() as session:
190
+ statement = select(Session).where(Session.user_id == user_id)
191
+ result = await self._execute_statement(session, statement)
192
+ return [
193
+ SessionResponse(**session.model_dump())
194
+ for session in result.scalars().all()
195
+ ]
196
+
197
+ async def remove_session(self, user_id: str, session_id: str) -> SessionResponse:
198
+ async with self._session_scope() as session:
199
+ statement = select(Session).where(
200
+ Session.user_id == user_id, Session.id == session_id
201
+ )
202
+ result = await self._execute_statement(session, statement)
203
+ session = result.scalar_one_or_none()
204
+ if not session:
205
+ raise NotFoundError(f"{self.entity_name} not found")
206
+ await self._execute_statement(
207
+ session,
208
+ delete(Session).where(
209
+ Session.id == session_id, Session.user_id == user_id
210
+ ),
211
+ )
212
+ return SessionResponse(**session.model_dump())
@@ -1,5 +1,6 @@
1
1
  from abc import ABC, abstractmethod
2
2
 
3
+ from my_app_name.schema.session import SessionResponse
3
4
  from my_app_name.schema.user import (
4
5
  User,
5
6
  UserCreateWithAudit,
@@ -18,6 +19,14 @@ class UserRepository(ABC):
18
19
  async def get_by_ids(self, id_list: list[str]) -> UserResponse:
19
20
  """Get users by ids"""
20
21
 
22
+ @abstractmethod
23
+ async def add_roles(self, data: dict[str, list[str]], created_by: str):
24
+ """Add roles to user"""
25
+
26
+ @abstractmethod
27
+ async def remove_all_roles(self, user_ids: list[str] = []):
28
+ """Remove roles from user"""
29
+
21
30
  @abstractmethod
22
31
  async def get(
23
32
  self,
@@ -61,3 +70,23 @@ class UserRepository(ABC):
61
70
  @abstractmethod
62
71
  async def get_by_credentials(self, username: str, password: str) -> UserResponse:
63
72
  """Get user by credential"""
73
+
74
+ @abstractmethod
75
+ async def get_by_token(self, token: str) -> UserResponse:
76
+ """Get user by token"""
77
+
78
+ @abstractmethod
79
+ async def add_token(self, user_id: str, token: str):
80
+ """Add token to user"""
81
+
82
+ @abstractmethod
83
+ async def remove_token(self, user_id: str, token: str):
84
+ """Remove token from user"""
85
+
86
+ @abstractmethod
87
+ async def get_sessions(self, user_id: str) -> list[SessionResponse]:
88
+ """Get sessions"""
89
+
90
+ @abstractmethod
91
+ async def remove_session(self, user_id: str, session_id: str) -> SessionResponse:
92
+ """Remove a session"""
@@ -6,9 +6,9 @@ from my_app_name.module.auth.service.user.repository.user_repository import (
6
6
  )
7
7
  from my_app_name.schema.user import (
8
8
  MultipleUserResponse,
9
- UserCreateWithAudit,
9
+ UserCreateWithRolesAndAudit,
10
10
  UserResponse,
11
- UserUpdateWithAudit,
11
+ UserUpdateWithRolesAndAudit,
12
12
  )
13
13
 
14
14
 
@@ -42,35 +42,57 @@ class UserService(BaseService):
42
42
  count = await self.user_repository.count(filter)
43
43
  return MultipleUserResponse(data=users, count=count)
44
44
 
45
- @BaseService.route(
46
- "/api/v1/users",
47
- methods=["post"],
48
- response_model=UserResponse,
49
- )
50
- async def create_user(self, data: UserCreateWithAudit) -> UserResponse:
51
- user = await self.user_repository.create(data)
52
- return await self.user_repository.get_by_id(user.id)
53
-
54
45
  @BaseService.route(
55
46
  "/api/v1/users/bulk",
56
47
  methods=["post"],
57
48
  response_model=list[UserResponse],
58
49
  )
59
50
  async def create_user_bulk(
60
- self, data: list[UserCreateWithAudit]
51
+ self, data: list[UserCreateWithRolesAndAudit]
61
52
  ) -> list[UserResponse]:
53
+ role_ids = [row.get_role_ids() for row in data]
54
+ data = [row.get_user_create_with_audit() for row in data]
62
55
  users = await self.user_repository.create_bulk(data)
56
+ if len(users) > 0:
57
+ created_by = users[0].created_by
58
+ await self.user_repository.add_roles(
59
+ data={user.id: role_ids[i] for i, user in enumerate(data)},
60
+ created_by=created_by,
61
+ )
63
62
  return await self.user_repository.get_by_ids([user.id for user in users])
64
63
 
64
+ @BaseService.route(
65
+ "/api/v1/users",
66
+ methods=["post"],
67
+ response_model=UserResponse,
68
+ )
69
+ async def create_user(self, data: UserCreateWithRolesAndAudit) -> UserResponse:
70
+ role_ids = data.get_role_ids()
71
+ data = data.get_user_create_with_audit()
72
+ user = await self.user_repository.create(data)
73
+ await self.user_repository.add_roles(
74
+ data={user.id: role_ids}, created_by=user.created_by
75
+ )
76
+ return await self.user_repository.get_by_id(user.id)
77
+
65
78
  @BaseService.route(
66
79
  "/api/v1/users/bulk",
67
80
  methods=["put"],
68
81
  response_model=UserResponse,
69
82
  )
70
83
  async def update_user_bulk(
71
- self, user_ids: list[str], data: UserUpdateWithAudit
84
+ self, user_ids: list[str], data: UserUpdateWithRolesAndAudit
72
85
  ) -> UserResponse:
86
+ role_ids = [row.get_role_ids() for row in data]
87
+ data = [row.get_user_create_with_audit() for row in data]
73
88
  users = await self.user_repository.update_bulk(user_ids, data)
89
+ if len(users) > 0:
90
+ updated_by = users[0].updated_by
91
+ await self.user_repository.remove_all_roles([user.id for user in users])
92
+ await self.user_repository.add_roles(
93
+ data={user.id: role_ids[i] for i, user in enumerate(data)},
94
+ updated_by=updated_by,
95
+ )
74
96
  return await self.user_repository.get_by_ids([user.id for user in users])
75
97
 
76
98
  @BaseService.route(
@@ -79,7 +101,7 @@ class UserService(BaseService):
79
101
  response_model=UserResponse,
80
102
  )
81
103
  async def update_user(
82
- self, user_id: str, data: UserUpdateWithAudit
104
+ self, user_id: str, data: UserUpdateWithRolesAndAudit
83
105
  ) -> UserResponse:
84
106
  user = await self.user_repository.update(user_id, data)
85
107
  return await self.user_repository.get_by_id(user.id)
@@ -8,15 +8,15 @@ from my_app_name.schema.permission import (
8
8
  )
9
9
  from my_app_name.schema.role import (
10
10
  MultipleRoleResponse,
11
- RoleCreate,
11
+ RoleCreateWithPermissions,
12
12
  RoleResponse,
13
- RoleUpdate,
13
+ RoleUpdateWithPermissions,
14
14
  )
15
15
  from my_app_name.schema.user import (
16
16
  MultipleUserResponse,
17
- UserCreate,
17
+ UserCreateWithRoles,
18
18
  UserResponse,
19
- UserUpdate,
19
+ UserUpdateWithRoles,
20
20
  )
21
21
 
22
22
 
@@ -44,7 +44,7 @@ def serve_auth_route(app: FastAPI):
44
44
  response_model=list[PermissionResponse],
45
45
  )
46
46
  async def create_permission_bulk(data: list[PermissionCreate]):
47
- return await auth_client.create_permission(
47
+ return await auth_client.create_permission_bulk(
48
48
  [row.with_audit(created_by="system") for row in data]
49
49
  )
50
50
 
@@ -108,8 +108,8 @@ def serve_auth_route(app: FastAPI):
108
108
  "/api/v1/roles/bulk",
109
109
  response_model=list[RoleResponse],
110
110
  )
111
- async def create_role_bulk(data: list[RoleCreate]):
112
- return await auth_client.create_role(
111
+ async def create_role_bulk(data: list[RoleCreateWithPermissions]):
112
+ return await auth_client.create_role_bulk(
113
113
  [row.with_audit(created_by="system") for row in data]
114
114
  )
115
115
 
@@ -117,14 +117,14 @@ def serve_auth_route(app: FastAPI):
117
117
  "/api/v1/roles",
118
118
  response_model=RoleResponse,
119
119
  )
120
- async def create_role(data: RoleCreate):
120
+ async def create_role(data: RoleCreateWithPermissions):
121
121
  return await auth_client.create_role(data.with_audit(created_by="system"))
122
122
 
123
123
  @app.put(
124
124
  "/api/v1/roles/bulk",
125
125
  response_model=list[RoleResponse],
126
126
  )
127
- async def update_role_bulk(role_ids: list[str], data: RoleUpdate):
127
+ async def update_role_bulk(role_ids: list[str], data: RoleUpdateWithPermissions):
128
128
  return await auth_client.update_role_bulk(
129
129
  role_ids, data.with_audit(updated_by="system")
130
130
  )
@@ -133,7 +133,7 @@ def serve_auth_route(app: FastAPI):
133
133
  "/api/v1/roles/{role_id}",
134
134
  response_model=RoleResponse,
135
135
  )
136
- async def update_role(role_id: str, data: RoleUpdate):
136
+ async def update_role(role_id: str, data: RoleUpdateWithPermissions):
137
137
  return await auth_client.update_role(data.with_audit(updated_by="system"))
138
138
 
139
139
  @app.delete(
@@ -171,7 +171,7 @@ def serve_auth_route(app: FastAPI):
171
171
  "/api/v1/users/bulk",
172
172
  response_model=list[UserResponse],
173
173
  )
174
- async def create_user_bulk(data: list[UserCreate]):
174
+ async def create_user_bulk(data: list[UserCreateWithRoles]):
175
175
  return await auth_client.create_user(
176
176
  [row.with_audit(created_by="system") for row in data]
177
177
  )
@@ -180,14 +180,14 @@ def serve_auth_route(app: FastAPI):
180
180
  "/api/v1/users",
181
181
  response_model=UserResponse,
182
182
  )
183
- async def create_user(data: UserCreate):
183
+ async def create_user(data: UserCreateWithRoles):
184
184
  return await auth_client.create_user(data.with_audit(created_by="system"))
185
185
 
186
186
  @app.put(
187
187
  "/api/v1/users/bulk",
188
188
  response_model=list[UserResponse],
189
189
  )
190
- async def update_user_bulk(user_ids: list[str], data: UserUpdate):
190
+ async def update_user_bulk(user_ids: list[str], data: UserUpdateWithRoles):
191
191
  return await auth_client.update_user_bulk(
192
192
  user_ids, data.with_audit(updated_by="system")
193
193
  )
@@ -196,7 +196,7 @@ def serve_auth_route(app: FastAPI):
196
196
  "/api/v1/users/{user_id}",
197
197
  response_model=UserResponse,
198
198
  )
199
- async def update_user(user_id: str, data: UserUpdate):
199
+ async def update_user(user_id: str, data: UserUpdateWithRoles):
200
200
  return await auth_client.update_user(data.with_audit(updated_by="system"))
201
201
 
202
202
  @app.delete(
@@ -47,5 +47,5 @@ class Permission(SQLModel, table=True):
47
47
  created_by: str | None = Field(index=True)
48
48
  updated_at: datetime.datetime | None = Field(index=True)
49
49
  updated_by: str | None = Field(index=True)
50
- name: str = Field(index=True)
50
+ name: str = Field(index=True, unique=True)
51
51
  description: str
@@ -22,7 +22,7 @@ class RoleCreateWithAudit(RoleCreate):
22
22
 
23
23
 
24
24
  class RoleCreateWithPermissions(RoleCreate):
25
- permissions: list[str] | None = None
25
+ permission_ids: list[str] | None = None
26
26
 
27
27
  def with_audit(self, created_by: str) -> "RoleCreateWithPermissionsAndAudit":
28
28
  return RoleCreateWithPermissionsAndAudit(
@@ -35,14 +35,16 @@ class RoleCreateWithPermissionsAndAudit(RoleCreateWithPermissions):
35
35
 
36
36
  def get_role_create_with_audit(self) -> RoleCreateWithAudit:
37
37
  data = {
38
- key: val for key, val in self.model_dump().items() if key != "permissions"
38
+ key: val
39
+ for key, val in self.model_dump().items()
40
+ if key != "permission_ids"
39
41
  }
40
42
  return RoleCreateWithAudit(**data)
41
43
 
42
- def get_permission_names(self) -> list[str]:
43
- if self.permissions is None:
44
+ def get_permission_ids(self) -> list[str]:
45
+ if self.permission_ids is None:
44
46
  return []
45
- return self.permissions
47
+ return self.permission_ids
46
48
 
47
49
 
48
50
  class RoleUpdate(SQLModel):
@@ -57,6 +59,32 @@ class RoleUpdateWithAudit(RoleUpdate):
57
59
  updated_by: str
58
60
 
59
61
 
62
+ class RoleUpdateWithPermissions(RoleUpdate):
63
+ permission_ids: list[str] | None = None
64
+
65
+ def with_audit(self, updated_by: str) -> "RoleUpdateWithPermissionsAndAudit":
66
+ return RoleUpdateWithPermissionsAndAudit(
67
+ **self.model_dump(), updated_by=updated_by
68
+ )
69
+
70
+
71
+ class RoleUpdateWithPermissionsAndAudit(RoleUpdateWithPermissions):
72
+ updated_by: str
73
+
74
+ def get_role_update_with_audit(self) -> RoleUpdateWithAudit:
75
+ data = {
76
+ key: val
77
+ for key, val in self.model_dump().items()
78
+ if key != "permission_ids"
79
+ }
80
+ return RoleUpdateWithAudit(**data)
81
+
82
+ def get_permission_ids(self) -> list[str]:
83
+ if self.permission_ids is None:
84
+ return []
85
+ return self.permission_ids
86
+
87
+
60
88
  class RoleResponse(RoleBase):
61
89
  id: str
62
90
  permissions: list[Permission]
@@ -73,7 +101,7 @@ class Role(SQLModel, table=True):
73
101
  created_by: str | None = Field(index=True)
74
102
  updated_at: datetime.datetime | None = Field(index=True)
75
103
  updated_by: str | None = Field(index=True)
76
- name: str = Field(index=True)
104
+ name: str = Field(index=True, unique=True)
77
105
  description: str
78
106
 
79
107
 
@@ -7,9 +7,7 @@ from sqlmodel import Field, SQLModel
7
7
 
8
8
  class SessionBase(SQLModel):
9
9
  user_id: str
10
- device: str
11
- os: str
12
- browser: str
10
+ token: str
13
11
  expired_at: datetime.datetime | None
14
12
 
15
13
 
@@ -47,6 +45,4 @@ class MultipleSessionResponse(BaseModel):
47
45
  class Session(SQLModel, table=True):
48
46
  id: str = Field(default_factory=lambda: ulid.new().str, primary_key=True)
49
47
  user_id: str = Field(index=True)
50
- device: str
51
- os: str
52
- browser: str
48
+ token: str = Field(index=True, unique=True)
@@ -13,7 +13,6 @@ class UserBase(SQLModel):
13
13
 
14
14
  class UserCreate(UserBase):
15
15
  password: str
16
- roles: list[str] | None = None
17
16
 
18
17
  def with_audit(self, created_by: str) -> "UserCreateWithAudit":
19
18
  return UserCreateWithAudit(**self.model_dump(), created_by=created_by)
@@ -23,6 +22,26 @@ class UserCreateWithAudit(UserCreate):
23
22
  created_by: str
24
23
 
25
24
 
25
+ class UserCreateWithRoles(UserCreate):
26
+ role_ids: list[str] | None = None
27
+
28
+ def with_audit(self, created_by: str) -> "UserCreateWithRolesAndAudit":
29
+ return UserCreateWithRolesAndAudit(**self.model_dump(), created_by=created_by)
30
+
31
+
32
+ class UserCreateWithRolesAndAudit(UserCreateWithRoles):
33
+ created_by: str
34
+
35
+ def get_user_create_with_audit(self) -> UserCreateWithAudit:
36
+ data = {key: val for key, val in self.model_dump().items() if key != "role_ids"}
37
+ return UserCreateWithAudit(**data)
38
+
39
+ def get_role_ids(self) -> list[str]:
40
+ if self.role_ids is None:
41
+ return []
42
+ return self.role_ids
43
+
44
+
26
45
  class UserUpdate(SQLModel):
27
46
  username: str | None = None
28
47
  password: str | None = None
@@ -35,6 +54,26 @@ class UserUpdateWithAudit(UserUpdate):
35
54
  updated_by: str
36
55
 
37
56
 
57
+ class UserUpdateWithRoles(UserUpdate):
58
+ role_ids: list[str] | None = None
59
+
60
+ def with_audit(self, updated_by: str) -> "UserUpdateWithRolesAndAudit":
61
+ return UserUpdateWithRolesAndAudit(**self.model_dump(), updated_by=updated_by)
62
+
63
+
64
+ class UserUpdateWithRolesAndAudit(UserUpdateWithRoles):
65
+ updated_by: str
66
+
67
+ def get_user_update_with_audit(self) -> UserUpdateWithAudit:
68
+ data = {key: val for key, val in self.model_dump().items() if key != "role_ids"}
69
+ return UserUpdateWithAudit(**data)
70
+
71
+ def get_role_ids(self) -> list[str]:
72
+ if self.role_ids is None:
73
+ return []
74
+ return self.role_ids
75
+
76
+
38
77
  class UserResponse(UserBase):
39
78
  id: str
40
79
  roles: list[Role]
@@ -52,7 +91,7 @@ class User(SQLModel, table=True):
52
91
  created_by: str = Field(index=True)
53
92
  updated_at: datetime.datetime | None = Field(index=True)
54
93
  updated_by: str | None = Field(index=True)
55
- username: str = Field(index=True)
94
+ username: str = Field(index=True, unique=True)
56
95
  password: str
57
96
 
58
97
 
zrb/builtin/todo.py CHANGED
@@ -294,6 +294,7 @@ def _get_default_stop_work_time_str() -> str:
294
294
  description="Todo.txt content",
295
295
  prompt="Todo.txt content (will override existing)",
296
296
  default_str=lambda _: _get_todo_txt_content(),
297
+ allow_positional_parsing=False,
297
298
  ),
298
299
  ],
299
300
  description="📝 Edit todo",
zrb/config.py CHANGED
@@ -77,13 +77,32 @@ WEB_AUTH_REFRESH_TOKEN_EXPIRE_MINUTES = int(
77
77
  os.getenv("ZRB_WEB_REFRESH_TOKEN_EXPIRE_MINUTES", "60")
78
78
  )
79
79
  LLM_MODEL = os.getenv("ZRB_LLM_MODEL", "ollama_chat/llama3.1")
80
- LLM_SYSTEM_PROMPT = os.getenv("ZRB_LLM_SYSTEM_PROMPT", "You are a helpful assistant")
80
+
81
+ _DEFAULT_PROMPT = """
82
+ You are a helpful assistant and you have access to several tools.
83
+ Your goal is to provide a final answer by executing a series of planning, actions, reasoning, and evaluations.
84
+
85
+ Breakdown user request into several actionable tasks. For example, when user ask about current weather on current location, you should get the current location first.
86
+
87
+ DO NOT TRY TO SIMULATE TOOL OUTPUT.
88
+
89
+ ERROR HANDLING
90
+ 1. If you receive an error, read the error message carefully and identify the specific issue.
91
+ 2. Adjust your response accordingly and perform the review process again before resubmitting.
92
+
93
+ REMINDER:
94
+ - ALWAYS double-check your response format and function arguments before submitting.
95
+ - DON'T make up answers.
96
+ """.strip()
97
+ LLM_SYSTEM_PROMPT = os.getenv("ZRB_LLM_SYSTEM_PROMPT", _DEFAULT_PROMPT)
98
+ LLM_HISTORY_DIR = os.getenv(
99
+ "ZRB_LLM_HISTORY_DIR", os.path.expanduser(os.path.join("~", ".zrb-llm-history"))
100
+ )
81
101
  LLM_HISTORY_FILE = os.getenv(
82
- "ZRB_LLM_HISTORY_FILE",
83
- os.path.expanduser(os.path.join("~", ".zrb-llm-history.json")),
102
+ "ZRB_LLM_HISTORY_FILE", os.path.join(LLM_HISTORY_DIR, "history.json")
84
103
  )
85
104
  LLM_ALLOW_ACCESS_SHELL = to_boolean(os.getenv("ZRB_LLM_ACCESS_FILE", "1"))
86
- LLM_ALLOW_ACCESS_WEB = to_boolean(os.getenv("ZRB_LLM_ACCESS_WEB", "1"))
105
+ LLM_ALLOW_ACCESS_INTERNET = to_boolean(os.getenv("ZRB_LLM_ACCESS_INTERNET", "1"))
87
106
  RAG_EMBEDDING_MODEL = os.getenv("ZRB_RAG_EMBEDDING_MODEL", "ollama/nomic-embed-text")
88
107
  RAG_CHUNK_SIZE = int(os.getenv("ZRB_RAG_CHUNK_SIZE", "1024"))
89
108
  RAG_OVERLAP = int(os.getenv("ZRB_RAG_OVERLAP", "128"))
zrb/input/any_input.py CHANGED
@@ -19,6 +19,11 @@ class AnyInput(ABC):
19
19
  def prompt_message(self) -> str:
20
20
  pass
21
21
 
22
+ @property
23
+ @abstractmethod
24
+ def allow_positional_parsing(self) -> bool:
25
+ pass
26
+
22
27
  @abstractmethod
23
28
  def to_html(self, shared_ctx: AnySharedContext) -> str:
24
29
  pass