1+ """Memory configuration and CRUD API endpoints for the app layer.
2+
3+ This module exposes HTTP endpoints under the `/memory` prefix. It follows the
4+ app-layer responsibilities:
5+ - Parse and validate HTTP inputs
6+ - Delegate business logic to the service layer
7+ - Convert unexpected exceptions to error JSON responses
8+
9+ Routes:
10+ - GET `/memory/config/load`: Load memory-related configuration for current user
11+ - POST `/memory/config/set`: Set a single configuration entry
12+ - POST `/memory/config/disable_agent`: Add a disabled agent id
13+ - DELETE `/memory/config/disable_agent/{agent_id}`: Remove a disabled agent id
14+ - POST `/memory/config/disable_useragent`: Add a disabled user-agent id
15+ - DELETE `/memory/config/disable_useragent/{agent_id}`: Remove a disabled user-agent id
16+ - POST `/memory/add`: Add memory items (optionally with LLM inference)
17+ - POST `/memory/search`: Semantic search memory items
18+ - GET `/memory/list`: List memory items
19+ - DELETE `/memory/delete/{memory_id}`: Delete a single memory item
20+ - DELETE `/memory/clear`: Clear memory items by scope
21+ """
122import asyncio
223import logging
324from typing import Any , Dict , List , Optional
425
5- from fastapi import APIRouter , Body , Header , Path , Query
26+ from http import HTTPStatus
27+ from fastapi import APIRouter , Body , Header , Path , Query , HTTPException
628from fastapi .responses import JSONResponse
29+
730from nexent .memory .memory_service import (
831 add_memory as svc_add_memory ,
932 clear_memory as svc_clear_memory ,
1033 delete_memory as svc_delete_memory ,
1134 list_memory as svc_list_memory ,
1235 search_memory as svc_search_memory ,
1336)
14-
1537from consts .const import (
1638 MEMORY_AGENT_SHARE_KEY ,
1739 MEMORY_SWITCH_KEY ,
1840 BOOLEAN_TRUE_VALUES ,
1941)
2042from consts .model import MemoryAgentShareMode
43+ from consts .exceptions import UnauthorizedError
2144from services .memory_config_service import (
2245 add_disabled_agent_id ,
2346 add_disabled_useragent_id ,
3558router = APIRouter (prefix = "/memory" )
3659
3760
38- # ---------------------------------------------------------------------------
39- # Generic helpers
40- # ---------------------------------------------------------------------------
41- def _success (message : str = "success" , content : Optional [Any ] = None ):
42- return JSONResponse (status_code = 200 , content = {"message" : message , "status" : "success" , "content" : content })
43-
44-
45- def _error (message : str = "error" ):
46- return JSONResponse (status_code = 400 , content = {"message" : message , "status" : "error" })
47-
48-
49- # ---------------------------------------------------------------------------
50- # Helper function
51- # ---------------------------------------------------------------------------
52-
53-
5461# ---------------------------------------------------------------------------
5562# Configuration Endpoints
5663# ---------------------------------------------------------------------------
5764@router .get ("/config/load" )
5865def load_configs (authorization : Optional [str ] = Header (None )):
59- """Load all memory-related configuration for current user."""
66+ """Load all memory-related configuration for the current user.
67+
68+ Args:
69+ authorization: Optional authorization header used to identify the user.
70+ """
6071 try :
6172 user_id , _ = get_current_user_id (authorization )
6273 configs = get_user_configs (user_id )
63- return _success (content = configs )
74+ return JSONResponse (status_code = HTTPStatus .OK , content = configs )
75+ except UnauthorizedError as e :
76+ raise HTTPException (status_code = HTTPStatus .UNAUTHORIZED , detail = str (e ))
6477 except Exception as e :
6578 logger .error ("load_configs failed: %s" , e )
66- return _error ("Failed to load configuration" )
79+ raise HTTPException (status_code = HTTPStatus .BAD_REQUEST ,
80+ detail = "Failed to load configuration" )
6781
6882
6983@router .post ("/config/set" )
@@ -72,7 +86,17 @@ def set_single_config(
7286 value : Any = Body (..., embed = True , description = "Configuration value" ),
7387 authorization : Optional [str ] = Header (None ),
7488):
75- """Unified endpoint to set single-value configuration items."""
89+ """Set a single-value configuration item for the current user.
90+
91+ Supported keys:
92+ - `MEMORY_SWITCH_KEY`: Toggle memory system on/off (boolean-like values accepted)
93+ - `MEMORY_AGENT_SHARE_KEY`: Set agent share mode (`always`/`ask`/`never`)
94+
95+ Args:
96+ key: Configuration key to update.
97+ value: New value for the configuration key.
98+ authorization: Optional authorization header used to identify the user.
99+ """
76100 user_id , _ = get_current_user_id (authorization )
77101
78102 if key == MEMORY_SWITCH_KEY :
@@ -83,52 +107,93 @@ def set_single_config(
83107 try :
84108 mode = MemoryAgentShareMode (str (value ))
85109 except ValueError :
86- return _error ("Invalid value for MEMORY_AGENT_SHARE (expected always/ask/never)" )
110+ raise HTTPException (status_code = HTTPStatus .NOT_ACCEPTABLE ,
111+ detail = "Invalid value for MEMORY_AGENT_SHARE (expected always/ask/never)" )
87112 ok = set_agent_share (user_id , mode )
88113 else :
89- return _error ("Unsupported configuration key" )
114+ raise HTTPException (status_code = HTTPStatus .NOT_ACCEPTABLE ,
115+ detail = "Unsupported configuration key" )
90116
91- return _success () if ok else _error ("Failed to update configuration" )
117+ if ok :
118+ return JSONResponse (status_code = HTTPStatus .OK , content = {"success" : True })
119+ raise HTTPException (status_code = HTTPStatus .BAD_REQUEST ,
120+ detail = "Failed to update configuration" )
92121
93122
94123@router .post ("/config/disable_agent" )
95124def add_disable_agent (
96125 agent_id : str = Body (..., embed = True ),
97126 authorization : Optional [str ] = Header (None ),
98127):
128+ """Add an agent id to the user's disabled agent list.
129+
130+ Args:
131+ agent_id: Identifier of the agent to disable.
132+ authorization: Optional authorization header used to identify the user.
133+ """
99134 user_id , _ = get_current_user_id (authorization )
100135 ok = add_disabled_agent_id (user_id , agent_id )
101- return _success () if ok else _error ("Failed to add disable agent id" )
136+ if ok :
137+ return JSONResponse (status_code = HTTPStatus .OK , content = {"success" : True })
138+ raise HTTPException (status_code = HTTPStatus .BAD_REQUEST ,
139+ detail = "Failed to add disable agent id" )
102140
103141
104142@router .delete ("/config/disable_agent/{agent_id}" )
105143def remove_disable_agent (
106144 agent_id : str = Path (...),
107145 authorization : Optional [str ] = Header (None ),
108146):
147+ """Remove an agent id from the user's disabled agent list.
148+
149+ Args:
150+ agent_id: Identifier of the agent to remove from the disabled list.
151+ authorization: Optional authorization header used to identify the user.
152+ """
109153 user_id , _ = get_current_user_id (authorization )
110154 ok = remove_disabled_agent_id (user_id , agent_id )
111- return _success () if ok else _error ("Failed to remove disable agent id" )
155+ if ok :
156+ return JSONResponse (status_code = HTTPStatus .OK , content = {"success" : True })
157+ raise HTTPException (status_code = HTTPStatus .BAD_REQUEST ,
158+ detail = "Failed to remove disable agent id" )
112159
113160
114161@router .post ("/config/disable_useragent" )
115162def add_disable_useragent (
116163 agent_id : str = Body (..., embed = True ),
117164 authorization : Optional [str ] = Header (None ),
118165):
166+ """Add a user-agent id to the user's disabled user-agent list.
167+
168+ Args:
169+ agent_id: Identifier of the user-agent to disable.
170+ authorization: Optional authorization header used to identify the user.
171+ """
119172 user_id , _ = get_current_user_id (authorization )
120173 ok = add_disabled_useragent_id (user_id , agent_id )
121- return _success () if ok else _error ("Failed to add disable user-agent id" )
174+ if ok :
175+ return JSONResponse (status_code = HTTPStatus .OK , content = {"success" : True })
176+ raise HTTPException (status_code = HTTPStatus .BAD_REQUEST ,
177+ detail = "Failed to add disable user-agent id" )
122178
123179
124180@router .delete ("/config/disable_useragent/{agent_id}" )
125181def remove_disable_useragent (
126182 agent_id : str = Path (...),
127183 authorization : Optional [str ] = Header (None ),
128184):
185+ """Remove a user-agent id from the user's disabled user-agent list.
186+
187+ Args:
188+ agent_id: Identifier of the user-agent to remove from the disabled list.
189+ authorization: Optional authorization header used to identify the user.
190+ """
129191 user_id , _ = get_current_user_id (authorization )
130192 ok = remove_disabled_useragent_id (user_id , agent_id )
131- return _success () if ok else _error ("Failed to remove disable user-agent id" )
193+ if ok :
194+ return JSONResponse (status_code = HTTPStatus .OK , content = {"success" : True })
195+ raise HTTPException (status_code = HTTPStatus .BAD_REQUEST ,
196+ detail = "Failed to remove disable user-agent id" )
132197
133198
134199# ---------------------------------------------------------------------------
@@ -145,6 +210,15 @@ def add_memory(
145210 True , embed = True , description = "Whether to run LLM inference during add" ),
146211 authorization : Optional [str ] = Header (None ),
147212):
213+ """Add memory records for the given scope.
214+
215+ Args:
216+ messages: List of chat messages as dictionaries.
217+ memory_level: Scope for the memory record (tenant/agent/user/user_agent).
218+ agent_id: Optional agent identifier when scope is agent-related.
219+ infer: Whether to run LLM inference during add.
220+ authorization: Optional authorization header used to identify the user.
221+ """
148222 user_id , tenant_id = get_current_user_id (authorization )
149223 try :
150224 result = asyncio .run (svc_add_memory (
@@ -156,10 +230,10 @@ def add_memory(
156230 agent_id = agent_id ,
157231 infer = infer ,
158232 ))
159- return _success ( content = result )
233+ return JSONResponse ( status_code = HTTPStatus . OK , content = result )
160234 except Exception as e :
161235 logger .error ("add_memory error: %s" , e , exc_info = True )
162- return _error ( str (e ))
236+ raise HTTPException ( status_code = HTTPStatus . BAD_REQUEST , detail = str (e ))
163237
164238
165239@router .post ("/search" )
@@ -170,6 +244,15 @@ def search_memory(
170244 agent_id : Optional [str ] = Body (None , embed = True ),
171245 authorization : Optional [str ] = Header (None ),
172246):
247+ """Search memory semantically for the given scope.
248+
249+ Args:
250+ query_text: Natural language query to search memory.
251+ memory_level: Scope for search (tenant/agent/user/user_agent).
252+ top_k: Maximum number of results to return.
253+ agent_id: Optional agent identifier when scope is agent-related.
254+ authorization: Optional authorization header used to identify the user.
255+ """
173256 user_id , tenant_id = get_current_user_id (authorization )
174257 try :
175258 results = asyncio .run (svc_search_memory (
@@ -181,10 +264,10 @@ def search_memory(
181264 top_k = top_k ,
182265 agent_id = agent_id ,
183266 ))
184- return _success ( content = results )
267+ return JSONResponse ( status_code = HTTPStatus . OK , content = results )
185268 except Exception as e :
186269 logger .error ("search_memory error: %s" , e , exc_info = True )
187- return _error ( str (e ))
270+ raise HTTPException ( status_code = HTTPStatus . BAD_REQUEST , detail = str (e ))
188271
189272
190273@router .get ("/list" )
@@ -195,6 +278,13 @@ def list_memory(
195278 None , description = "Filter by agent id if applicable" ),
196279 authorization : Optional [str ] = Header (None ),
197280):
281+ """List memory for the given scope.
282+
283+ Args:
284+ memory_level: Scope for listing (tenant/agent/user/user_agent).
285+ agent_id: Optional agent filter when scope is agent-related.
286+ authorization: Optional authorization header used to identify the user.
287+ """
198288 user_id , tenant_id = get_current_user_id (authorization )
199289 try :
200290 payload = asyncio .run (svc_list_memory (
@@ -204,25 +294,31 @@ def list_memory(
204294 user_id = user_id ,
205295 agent_id = agent_id ,
206296 ))
207- return _success ( content = payload )
297+ return JSONResponse ( status_code = HTTPStatus . OK , content = payload )
208298 except Exception as e :
209299 logger .error ("list_memory error: %s" , e , exc_info = True )
210- return _error ( str (e ))
300+ raise HTTPException ( status_code = HTTPStatus . BAD_REQUEST , detail = str (e ))
211301
212302
213303@router .delete ("/delete/{memory_id}" )
214304def delete_memory (
215305 memory_id : str = Path (..., description = "ID of memory to delete" ),
216306 authorization : Optional [str ] = Header (None ),
217307):
308+ """Delete a specific memory record by id.
309+
310+ Args:
311+ memory_id: Identifier of the memory record to delete.
312+ authorization: Optional authorization header used to identify the user.
313+ """
218314 _user_id , tenant_id = get_current_user_id (authorization )
219315 try :
220316 result = asyncio .run (svc_delete_memory (
221317 memory_id = memory_id , memory_config = build_memory_config (tenant_id )))
222- return _success ( content = result )
318+ return JSONResponse ( status_code = HTTPStatus . OK , content = result )
223319 except Exception as e :
224320 logger .error ("delete_memory error: %s" , e , exc_info = True )
225- return _error ( str (e ))
321+ raise HTTPException ( status_code = HTTPStatus . BAD_REQUEST , detail = str (e ))
226322
227323
228324@router .delete ("/clear" )
@@ -233,6 +329,13 @@ def clear_memory(
233329 None , description = "Filter by agent id if applicable" ),
234330 authorization : Optional [str ] = Header (None ),
235331):
332+ """Clear memory records for the given scope.
333+
334+ Args:
335+ memory_level: Scope for clearing (tenant/agent/user/user_agent).
336+ agent_id: Optional agent filter when scope is agent-related.
337+ authorization: Optional authorization header used to identify the user.
338+ """
236339 user_id , tenant_id = get_current_user_id (authorization )
237340 try :
238341 result = asyncio .run (svc_clear_memory (
@@ -242,7 +345,7 @@ def clear_memory(
242345 user_id = user_id ,
243346 agent_id = agent_id ,
244347 ))
245- return _success ( content = result )
348+ return JSONResponse ( status_code = HTTPStatus . OK , content = result )
246349 except Exception as e :
247350 logger .error ("clear_memory error: %s" , e , exc_info = True )
248- return _error ( str (e ))
351+ raise HTTPException ( status_code = HTTPStatus . BAD_REQUEST , detail = str (e ))
0 commit comments