Skip to content

Commit 2d6ea62

Browse files
committed
feat: add time filtering to memory query and update related components
1 parent e6b2459 commit 2d6ea62

6 files changed

Lines changed: 93 additions & 30 deletions

File tree

app/src/main/java/com/ai/assistance/operit/core/config/SystemToolPrompts.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -627,10 +627,12 @@ object SystemToolPrompts {
627627
tools = listOf(
628628
ToolPrompt(
629629
name = "query_memory",
630-
description = "Searches the memory library for relevant memories using hybrid search (keyword matching + semantic understanding). Use this when you need to recall past knowledge, look up specific information, or require context. Keywords can be separated by '|' or spaces - each keyword will be independently matched semantically and the results will be combined with weighted scoring. You can use \"*\" as the query to return all memories (optionally filtered by folder_path). When the user attaches a memory folder, a `<memory_context>` will be provided in the prompt. You MUST use the `folder_path` parameter to restrict the search to that folder. **IMPORTANT**: For document nodes (uploaded files), this tool uses vector search to return ONLY the most relevant chunks matching your query, NOT the entire document. Results show \"Document: [name], Chunk X/Y: [content]\" format. To read the complete document or specific parts, use `get_memory_by_title` instead. **NOTE**: When limit > 20, results will only show titles and truncated content to save tokens.",
630+
description = "Searches the memory library for relevant memories using hybrid search (keyword matching + semantic understanding). Use this when you need to recall past knowledge, look up specific information, or require context. Keywords can be separated by '|' or spaces - each keyword will be independently matched semantically and the results will be combined with weighted scoring. You can use \"*\" as the query to return all memories (optionally filtered by folder_path). You can also filter by creation time using `start_time` / `end_time` (Unix milliseconds). When the user attaches a memory folder, a `<memory_context>` will be provided in the prompt. You MUST use the `folder_path` parameter to restrict the search to that folder. **IMPORTANT**: For document nodes (uploaded files), this tool uses vector search to return ONLY the most relevant chunks matching your query, NOT the entire document. Results show \"Document: [name], Chunk X/Y: [content]\" format. To read the complete document or specific parts, use `get_memory_by_title` instead. **NOTE**: When limit > 20, results will only show titles and truncated content to save tokens.",
631631
parametersStructured = listOf(
632632
ToolParameterSchema(name = "query", type = "string", description = "string, the keyword or question to search for, or \"*\" to return all memories", required = true),
633633
ToolParameterSchema(name = "folder_path", type = "string", description = "optional, string, the specific folder path to search within", required = false),
634+
ToolParameterSchema(name = "start_time", type = "integer", description = "optional, Unix timestamp in milliseconds. Filters memories by createdAt >= start_time", required = false),
635+
ToolParameterSchema(name = "end_time", type = "integer", description = "optional, Unix timestamp in milliseconds. Filters memories by createdAt <= end_time", required = false),
634636
ToolParameterSchema(name = "threshold", type = "number", description = "optional, float 0.0-1.0, semantic similarity threshold, lower values return more results", required = false, default = "0.25"),
635637
ToolParameterSchema(name = "limit", type = "integer", description = "optional, int >= 1, maximum number of results to return. When > 20, only titles and truncated content are returned", required = false, default = "5")
636638
)
@@ -710,10 +712,12 @@ object SystemToolPrompts {
710712
tools = listOf(
711713
ToolPrompt(
712714
name = "query_memory",
713-
description = "使用混合搜索(关键词匹配 + 语义理解)从记忆库中搜索相关记忆。当需要回忆过去的知识、查找特定信息或需要上下文时使用。关键词可以使用\"|\"或空格分隔 - 每个关键词都会独立进行语义匹配,结果将通过加权评分合并。可以使用 \"*\" 作为查询来返回所有记忆(可通过 folder_path 过滤)。当用户附加记忆文件夹时,提示中会提供`<memory_context>`。你必须使用 `folder_path` 参数将搜索限制在该文件夹内。**重要**:对于文档节点(上传的文件),此工具使用向量搜索只返回与查询最相关的分块,而不是整个文档。结果显示\"Document: [文档名], Chunk X/Y: [内容]\"格式。如需阅读完整文档或特定部分,请改用 `get_memory_by_title` 工具。**注意**:当 limit > 20 时,结果将只显示标题和截断内容以节省令牌。",
715+
description = "使用混合搜索(关键词匹配 + 语义理解)从记忆库中搜索相关记忆。当需要回忆过去的知识、查找特定信息或需要上下文时使用。关键词可以使用\"|\"或空格分隔 - 每个关键词都会独立进行语义匹配,结果将通过加权评分合并。可以使用 \"*\" 作为查询来返回所有记忆(可通过 folder_path 过滤)。也可以使用 `start_time` / `end_time`(Unix 毫秒时间戳)按创建时间过滤。当用户附加记忆文件夹时,提示中会提供`<memory_context>`。你必须使用 `folder_path` 参数将搜索限制在该文件夹内。**重要**:对于文档节点(上传的文件),此工具使用向量搜索只返回与查询最相关的分块,而不是整个文档。结果显示\"Document: [文档名], Chunk X/Y: [内容]\"格式。如需阅读完整文档或特定部分,请改用 `get_memory_by_title` 工具。**注意**:当 limit > 20 时,结果将只显示标题和截断内容以节省令牌。",
714716
parametersStructured = listOf(
715717
ToolParameterSchema(name = "query", type = "string", description = "string, 搜索的关键词或问题, 或使用 \"*\" 返回所有记忆", required = true),
716718
ToolParameterSchema(name = "folder_path", type = "string", description = "可选, string, 要搜索的特定文件夹路径", required = false),
719+
ToolParameterSchema(name = "start_time", type = "integer", description = "可选, Unix时间戳(毫秒)。按创建时间过滤 createdAt >= start_time", required = false),
720+
ToolParameterSchema(name = "end_time", type = "integer", description = "可选, Unix时间戳(毫秒)。按创建时间过滤 createdAt <= end_time", required = false),
717721
ToolParameterSchema(name = "threshold", type = "number", description = "可选, float 0.0-1.0, 语义相似度阈值, 较低的值返回更多结果", required = false, default = "0.25"),
718722
ToolParameterSchema(name = "limit", type = "integer", description = "可选, int >= 1, 返回结果的最大数量. 当 > 20 时,只返回标题和截断内容", required = false, default = "5")
719723
)

app/src/main/java/com/ai/assistance/operit/core/tools/defaultTool/standard/MemoryQueryToolExecutor.kt

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,37 @@ class MemoryQueryToolExecutor(private val context: Context) : ToolExecutor {
5757
val threshold = tool.parameters.find { it.name == "threshold" }?.value?.toFloatOrNull() ?: 0.25f
5858
val limitParam = tool.parameters.find { it.name == "limit" }?.value
5959
val limit = limitParam?.toIntOrNull()
60+
val startTimeParam = tool.parameters.find { it.name == "start_time" }?.value
61+
val endTimeParam = tool.parameters.find { it.name == "end_time" }?.value
62+
63+
val startTimeMs = startTimeParam?.takeIf { it.isNotBlank() }?.toLongOrNull()
64+
if (!startTimeParam.isNullOrBlank() && startTimeMs == null) {
65+
return ToolResult(
66+
toolName = tool.name,
67+
success = false,
68+
result = StringResultData(""),
69+
error = "Invalid start_time. Expected Unix timestamp in milliseconds."
70+
)
71+
}
72+
73+
val endTimeMs = endTimeParam?.takeIf { it.isNotBlank() }?.toLongOrNull()
74+
if (!endTimeParam.isNullOrBlank() && endTimeMs == null) {
75+
return ToolResult(
76+
toolName = tool.name,
77+
success = false,
78+
result = StringResultData(""),
79+
error = "Invalid end_time. Expected Unix timestamp in milliseconds."
80+
)
81+
}
82+
83+
if (startTimeMs != null && endTimeMs != null && startTimeMs > endTimeMs) {
84+
return ToolResult(
85+
toolName = tool.name,
86+
success = false,
87+
result = StringResultData(""),
88+
error = "Invalid time range: start_time must be <= end_time."
89+
)
90+
}
6091

6192
// 如果查询是 "*" 且用户没有显式指定 limit,则返回所有结果
6293
val isWildcardQuery = query.trim() == "*"
@@ -76,13 +107,18 @@ class MemoryQueryToolExecutor(private val context: Context) : ToolExecutor {
76107
// limit 无上限,但至少为 1
77108
val validLimit = if (finalLimit < 1) 1 else finalLimit
78109

79-
AppLogger.d(TAG, "Executing memory query: '$query' in folder: '${folderPath ?: "All"}', threshold: $validThreshold, limit: $validLimit")
110+
AppLogger.d(
111+
TAG,
112+
"Executing memory query: '$query' in folder: '${folderPath ?: "All"}', start_time: ${startTimeMs ?: "null"}, end_time: ${endTimeMs ?: "null"}, threshold: $validThreshold, limit: $validLimit"
113+
)
80114

81115
return try {
82116
val results = memoryRepository.searchMemories(
83117
query = query,
84118
folderPath = folderPath,
85-
semanticThreshold = validThreshold
119+
semanticThreshold = validThreshold,
120+
createdAtStartMs = startTimeMs,
121+
createdAtEndMs = endTimeMs
86122
)
87123

88124
val formattedResult = buildResultData(results.take(validLimit), query, validLimit)

app/src/main/java/com/ai/assistance/operit/core/tools/javascript/JsTools.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,9 +309,11 @@ fun getJsToolsDefinition(): String {
309309
// 记忆管理
310310
Memory: {
311311
// 查询记忆库
312-
query: (query, folderPath, threshold, limit) => {
312+
query: (query, folderPath, threshold, limit, startTime, endTime) => {
313313
const params = { query };
314314
if (folderPath) params.folder_path = folderPath;
315+
if (startTime !== undefined) params.start_time = startTime;
316+
if (endTime !== undefined) params.end_time = endTime;
315317
if (threshold !== undefined) params.threshold = threshold;
316318
if (limit !== undefined) params.limit = limit;
317319
return toolCall("query_memory", params);

app/src/main/java/com/ai/assistance/operit/data/repository/MemoryRepository.kt

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -440,28 +440,39 @@ class MemoryRepository(private val context: Context, profileId: String) {
440440
suspend fun searchMemories(
441441
query: String,
442442
folderPath: String? = null,
443-
semanticThreshold: Float = 0.6f
443+
semanticThreshold: Float = 0.6f,
444+
createdAtStartMs: Long? = null,
445+
createdAtEndMs: Long? = null
444446
): List<Memory> = withContext(Dispatchers.IO) {
445447
val normalizedFolderPath = normalizeFolderPath(folderPath)
446-
// 支持通配符搜索:如果查询是 "*",返回所有记忆(在文件夹过滤后)
447-
if (query.trim() == "*") {
448-
return@withContext if (normalizedFolderPath == null) {
449-
if (folderPath == "未分类") {
450-
memoryBox.all.filter { normalizeFolderPath(it.folderPath) == null }
451-
} else {
452-
memoryBox.all
453-
}
448+
449+
val memoriesInScope = if (normalizedFolderPath == null) {
450+
if (folderPath == "未分类") {
451+
memoryBox.all.filter { normalizeFolderPath(it.folderPath) == null }
454452
} else {
455-
getMemoriesByFolderPath(normalizedFolderPath)
453+
memoryBox.all
456454
}
455+
} else {
456+
getMemoriesByFolderPath(normalizedFolderPath)
457+
}
458+
459+
val timeFilteredMemoriesInScope = if (createdAtStartMs == null && createdAtEndMs == null) {
460+
memoriesInScope
461+
} else {
462+
memoriesInScope.filter { memory ->
463+
val createdAtMs = memory.createdAt.time
464+
(createdAtStartMs == null || createdAtMs >= createdAtStartMs) &&
465+
(createdAtEndMs == null || createdAtMs <= createdAtEndMs)
466+
}
467+
}
468+
469+
// 支持通配符搜索:如果查询是 "*",返回所有记忆(在文件夹过滤后)
470+
if (query.trim() == "*") {
471+
return@withContext timeFilteredMemoriesInScope
457472
}
458473

459474
if (query.isBlank()) {
460-
return@withContext if (normalizedFolderPath == null) {
461-
memoryBox.all.filter { normalizeFolderPath(it.folderPath) == null }
462-
} else {
463-
getMemoriesByFolderPath(normalizedFolderPath)
464-
}
475+
return@withContext timeFilteredMemoriesInScope
465476
}
466477

467478
// 支持两种分隔符:'|' 或空格
@@ -480,15 +491,7 @@ class MemoryRepository(private val context: Context, profileId: String) {
480491
// --- PRE-FILTERING BY FOLDER ---
481492
// If a folder path is provided, all subsequent searches will be performed on this subset.
482493
// Otherwise, search all memories.
483-
val memoriesToSearch = if (normalizedFolderPath == null) {
484-
if (folderPath == "未分类") {
485-
memoryBox.all.filter { normalizeFolderPath(it.folderPath) == null }
486-
} else {
487-
memoryBox.all
488-
}
489-
} else {
490-
getMemoriesByFolderPath(normalizedFolderPath)
491-
}
494+
val memoriesToSearch = timeFilteredMemoriesInScope
492495

493496
if (memoriesToSearch.isEmpty()) {
494497
com.ai.assistance.operit.util.AppLogger.d("MemoryRepo", "No memories found in folder '$folderPath' to search.")

app/src/main/java/com/ai/assistance/operit/services/floating/FloatingWindowManager.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import androidx.lifecycle.setViewTreeViewModelStoreOwner
6161
import androidx.savedstate.SavedStateRegistryOwner
6262
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
6363
import com.ai.assistance.operit.R
64+
import com.ai.assistance.operit.api.chat.AIForegroundService
6465
import com.ai.assistance.operit.data.model.AttachmentInfo
6566
import com.ai.assistance.operit.data.model.ChatMessage
6667
import com.ai.assistance.operit.data.model.InputProcessingState
@@ -646,6 +647,11 @@ class FloatingWindowManager(
646647
cancelFocusBeforeExit()
647648
}
648649

650+
val wasFullscreen =
651+
state.currentMode.value == FloatingMode.FULLSCREEN ||
652+
state.currentMode.value == FloatingMode.SCREEN_OCR
653+
val willFullscreen = newMode == FloatingMode.FULLSCREEN || newMode == FloatingMode.SCREEN_OCR
654+
649655
// 取消之前的动画
650656
sizeAnimator?.cancel()
651657

@@ -693,6 +699,16 @@ class FloatingWindowManager(
693699
state.currentMode.value = newMode
694700
callback.saveState()
695701

702+
if (wasFullscreen != willFullscreen) {
703+
try {
704+
AIForegroundService.setWakeListeningSuspendedForFloatingFullscreen(
705+
context.applicationContext,
706+
willFullscreen
707+
)
708+
} catch (_: Exception) {
709+
}
710+
}
711+
696712
// 计算目标尺寸和位置
697713
data class TargetParams(
698714
val width: Int,

examples/types/memory.d.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ export namespace Memory {
1313
* @param folderPath - Optional folder path to search within
1414
* @param threshold - Optional semantic similarity threshold (0.0-1.0, default 0.35)
1515
* @param limit - Optional maximum number of results (1-20, default 5)
16+
* @param startTime - Optional start time (Unix timestamp in milliseconds), filters memories by createdAt >= startTime
17+
* @param endTime - Optional end time (Unix timestamp in milliseconds), filters memories by createdAt <= endTime
1618
* @returns Query results as a string
1719
*/
18-
function query(query: string, folderPath?: string, threshold?: number, limit?: number): Promise<string>;
20+
function query(query: string, folderPath?: string, threshold?: number, limit?: number, startTime?: number, endTime?: number): Promise<string>;
1921

2022
/**
2123
* Get a memory by exact title

0 commit comments

Comments
 (0)