feat: agent memory management — CRUD API + Android management screen
Backend:
- New /api/v1/agents/{id}/memory endpoints: CRUD for global_knowledge,
knowledge_entities, learning_patterns, vector_memories + import/export
- Fix scope_id column overflow: 3 model columns expanded to hold compound
keys (user_id:agent_id format, 73 chars vs old VARCHAR(36))
- Config: allow unknown env vars (extra="ignore") for optional overrides
Android:
- MemoryManageScreen: 4-tab UI (全局知识/知识实体/学习模式/对话记忆)
with search, delete, and FAB to add new entries
- Import/export via ShareSheet and file picker
- AgentListScreen: long-press dropdown menu → 记忆管理 entry point
- NavGraph: memory_manage/{agentId}/{agentName} route with URL encoding
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -112,4 +112,98 @@ interface ApiService {
|
||||
@Path("agentId") agentId: String,
|
||||
@Query("limit") limit: Int = 50
|
||||
): SessionListResponse
|
||||
|
||||
// ─── Agent Memory ───
|
||||
@GET("api/v1/agents/{agentId}/memory/global-knowledge")
|
||||
suspend fun getGlobalKnowledge(
|
||||
@Path("agentId") agentId: String,
|
||||
@Query("skip") skip: Int = 0,
|
||||
@Query("limit") limit: Int = 50,
|
||||
@Query("search") search: String? = null
|
||||
): GlobalKnowledgeListResponse
|
||||
|
||||
@POST("api/v1/agents/{agentId}/memory/global-knowledge")
|
||||
suspend fun createGlobalKnowledge(
|
||||
@Path("agentId") agentId: String,
|
||||
@Body request: GlobalKnowledgeCreateRequest
|
||||
): GlobalKnowledgeDto
|
||||
|
||||
@PUT("api/v1/agents/{agentId}/memory/global-knowledge/{knowledgeId}")
|
||||
suspend fun updateGlobalKnowledge(
|
||||
@Path("agentId") agentId: String,
|
||||
@Path("knowledgeId") knowledgeId: String,
|
||||
@Body request: GlobalKnowledgeCreateRequest
|
||||
): GlobalKnowledgeDto
|
||||
|
||||
@DELETE("api/v1/agents/{agentId}/memory/global-knowledge/{knowledgeId}")
|
||||
suspend fun deleteGlobalKnowledge(
|
||||
@Path("agentId") agentId: String,
|
||||
@Path("knowledgeId") knowledgeId: String
|
||||
): Response<Unit>
|
||||
|
||||
@GET("api/v1/agents/{agentId}/memory/knowledge-entities")
|
||||
suspend fun getKnowledgeEntities(
|
||||
@Path("agentId") agentId: String,
|
||||
@Query("skip") skip: Int = 0,
|
||||
@Query("limit") limit: Int = 50,
|
||||
@Query("search") search: String? = null
|
||||
): KnowledgeEntityListResponse
|
||||
|
||||
@POST("api/v1/agents/{agentId}/memory/knowledge-entities")
|
||||
suspend fun createKnowledgeEntity(
|
||||
@Path("agentId") agentId: String,
|
||||
@Body request: KnowledgeEntityCreateRequest
|
||||
): KnowledgeEntityDto
|
||||
|
||||
@PUT("api/v1/agents/{agentId}/memory/knowledge-entities/{entityId}")
|
||||
suspend fun updateKnowledgeEntity(
|
||||
@Path("agentId") agentId: String,
|
||||
@Path("entityId") entityId: String,
|
||||
@Body request: KnowledgeEntityCreateRequest
|
||||
): KnowledgeEntityDto
|
||||
|
||||
@DELETE("api/v1/agents/{agentId}/memory/knowledge-entities/{entityId}")
|
||||
suspend fun deleteKnowledgeEntity(
|
||||
@Path("agentId") agentId: String,
|
||||
@Path("entityId") entityId: String
|
||||
): Response<Unit>
|
||||
|
||||
@GET("api/v1/agents/{agentId}/memory/learning-patterns")
|
||||
suspend fun getLearningPatterns(
|
||||
@Path("agentId") agentId: String,
|
||||
@Query("skip") skip: Int = 0,
|
||||
@Query("limit") limit: Int = 50
|
||||
): LearningPatternListResponse
|
||||
|
||||
@DELETE("api/v1/agents/{agentId}/memory/learning-patterns/{patternId}")
|
||||
suspend fun deleteLearningPattern(
|
||||
@Path("agentId") agentId: String,
|
||||
@Path("patternId") patternId: String
|
||||
): Response<Unit>
|
||||
|
||||
@POST("api/v1/agents/{agentId}/memory/export")
|
||||
suspend fun exportMemory(
|
||||
@Path("agentId") agentId: String
|
||||
): MemoryExportResponse
|
||||
|
||||
@POST("api/v1/agents/{agentId}/memory/import")
|
||||
suspend fun importMemory(
|
||||
@Path("agentId") agentId: String,
|
||||
@Body request: MemoryImportRequest
|
||||
): Response<Unit>
|
||||
|
||||
// ─── Vector Memories ───
|
||||
@GET("api/v1/agents/{agentId}/memory/vector-memories")
|
||||
suspend fun getVectorMemories(
|
||||
@Path("agentId") agentId: String,
|
||||
@Query("skip") skip: Int = 0,
|
||||
@Query("limit") limit: Int = 50,
|
||||
@Query("search") search: String? = null
|
||||
): VectorMemoryListResponse
|
||||
|
||||
@DELETE("api/v1/agents/{agentId}/memory/vector-memories/{memoryId}")
|
||||
suspend fun deleteVectorMemory(
|
||||
@Path("agentId") agentId: String,
|
||||
@Path("memoryId") memoryId: String
|
||||
): Response<Unit>
|
||||
}
|
||||
|
||||
@@ -60,11 +60,11 @@ class SseClient @Inject constructor(
|
||||
request: ChatRequest
|
||||
): Flow<SseEvent> = callbackFlow {
|
||||
// callbackFlow lambda is suspend — no runBlocking needed
|
||||
val baseUrl = tokenDataStore.serverUrl.first()
|
||||
val baseUrl = tokenDataStore.serverUrl.first().trimEnd('/')
|
||||
val url = if (agentId != null) {
|
||||
"${baseUrl}api/v1/agent-chat/$agentId/stream"
|
||||
"${baseUrl}/api/v1/agent-chat/$agentId/stream"
|
||||
} else {
|
||||
"${baseUrl}api/v1/agent-chat/bare/stream"
|
||||
"${baseUrl}/api/v1/agent-chat/bare/stream"
|
||||
}
|
||||
|
||||
val jsonBody = gson.toJson(request)
|
||||
|
||||
@@ -336,3 +336,102 @@ data class UploadResponse(
|
||||
val size: Int,
|
||||
@SerializedName("content_type") val contentType: String? = null
|
||||
)
|
||||
|
||||
// ─────────── Agent Memory ───────────
|
||||
|
||||
data class GlobalKnowledgeDto(
|
||||
val id: String,
|
||||
val content: String,
|
||||
@SerializedName("source_agent_id") val sourceAgentId: String? = null,
|
||||
val tags: List<String>? = null,
|
||||
val confidence: String = "medium",
|
||||
@SerializedName("expires_at") val expiresAt: String? = null,
|
||||
@SerializedName("created_at") val createdAt: String? = null
|
||||
)
|
||||
|
||||
data class GlobalKnowledgeListResponse(
|
||||
val items: List<GlobalKnowledgeDto>,
|
||||
val total: Int
|
||||
)
|
||||
|
||||
data class GlobalKnowledgeCreateRequest(
|
||||
val content: String,
|
||||
val tags: List<String>? = null,
|
||||
val confidence: String = "medium"
|
||||
)
|
||||
|
||||
data class KnowledgeEntityDto(
|
||||
val id: String,
|
||||
val name: String,
|
||||
@SerializedName("entity_type") val entityType: String = "concept",
|
||||
val description: String? = null,
|
||||
val source: String = "extracted",
|
||||
val confidence: String = "medium",
|
||||
val tags: List<String>? = null,
|
||||
@SerializedName("created_at") val createdAt: String? = null
|
||||
)
|
||||
|
||||
data class KnowledgeEntityListResponse(
|
||||
val items: List<KnowledgeEntityDto>,
|
||||
val total: Int
|
||||
)
|
||||
|
||||
data class KnowledgeEntityCreateRequest(
|
||||
val name: String,
|
||||
@SerializedName("entity_type") val entityType: String = "concept",
|
||||
val description: String? = null,
|
||||
val tags: List<String>? = null,
|
||||
val confidence: String = "medium"
|
||||
)
|
||||
|
||||
data class LearningPatternDto(
|
||||
val id: String,
|
||||
@SerializedName("task_category") val taskCategory: String = "",
|
||||
@SerializedName("task_keywords") val taskKeywords: String? = null,
|
||||
@SerializedName("suggested_tools") val suggestedTools: String? = null,
|
||||
@SerializedName("effectiveness_score") val effectivenessScore: Double = 0.0,
|
||||
@SerializedName("total_runs") val totalRuns: Int = 0,
|
||||
@SerializedName("successful_runs") val successfulRuns: Int = 0,
|
||||
@SerializedName("avg_iterations") val avgIterations: Double = 0.0,
|
||||
@SerializedName("avg_tool_calls") val avgToolCalls: Double = 0.0,
|
||||
@SerializedName("last_used_at") val lastUsedAt: String? = null,
|
||||
@SerializedName("created_at") val createdAt: String? = null
|
||||
)
|
||||
|
||||
data class LearningPatternListResponse(
|
||||
val items: List<LearningPatternDto>,
|
||||
val total: Int
|
||||
)
|
||||
|
||||
data class MemoryExportResponse(
|
||||
@SerializedName("agent_id") val agentId: String,
|
||||
@SerializedName("exported_at") val exportedAt: String,
|
||||
@SerializedName("global_knowledge") val globalKnowledge: List<Map<String, Any?>> = emptyList(),
|
||||
@SerializedName("knowledge_entities") val knowledgeEntities: List<Map<String, Any?>> = emptyList(),
|
||||
@SerializedName("knowledge_relations") val knowledgeRelations: List<Map<String, Any?>> = emptyList(),
|
||||
@SerializedName("learning_patterns") val learningPatterns: List<Map<String, Any?>> = emptyList(),
|
||||
@SerializedName("vector_memories") val vectorMemories: List<Map<String, Any?>> = emptyList()
|
||||
)
|
||||
|
||||
data class MemoryImportRequest(
|
||||
@SerializedName("global_knowledge") val globalKnowledge: List<Map<String, Any?>> = emptyList(),
|
||||
@SerializedName("knowledge_entities") val knowledgeEntities: List<Map<String, Any?>> = emptyList(),
|
||||
@SerializedName("knowledge_relations") val knowledgeRelations: List<Map<String, Any?>> = emptyList(),
|
||||
@SerializedName("learning_patterns") val learningPatterns: List<Map<String, Any?>> = emptyList(),
|
||||
@SerializedName("vector_memories") val vectorMemories: List<Map<String, Any?>> = emptyList()
|
||||
)
|
||||
|
||||
// ─────────── Vector Memories ───────────
|
||||
|
||||
data class VectorMemoryDto(
|
||||
val id: String,
|
||||
@SerializedName("content_text") val contentText: String,
|
||||
@SerializedName("session_key") val sessionKey: String = "",
|
||||
val metadata: Map<String, Any?>? = null,
|
||||
@SerializedName("created_at") val createdAt: String? = null
|
||||
)
|
||||
|
||||
data class VectorMemoryListResponse(
|
||||
val items: List<VectorMemoryDto>,
|
||||
val total: Int
|
||||
)
|
||||
|
||||
@@ -23,6 +23,7 @@ import com.tiangong.aiagent.ui.common.SkeletonAgentList
|
||||
fun AgentListScreen(
|
||||
onBack: () -> Unit,
|
||||
onAgentSelected: (com.tiangong.aiagent.domain.model.Agent) -> Unit,
|
||||
onManageMemory: (agentId: String, agentName: String) -> Unit = { _, _ -> },
|
||||
viewModel: AgentListViewModel = hiltViewModel()
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
@@ -108,12 +109,34 @@ fun AgentListScreen(
|
||||
contentPadding = PaddingValues(vertical = 8.dp)
|
||||
) {
|
||||
items(uiState.agents) { agent ->
|
||||
var showMenu by remember { mutableStateOf(false) }
|
||||
var showDeleteDialog by remember { mutableStateOf(false) }
|
||||
AgentListItem(
|
||||
agent = agent,
|
||||
onClick = { onAgentSelected(agent) },
|
||||
onLongClick = { showDeleteDialog = true }
|
||||
)
|
||||
Box {
|
||||
AgentListItem(
|
||||
agent = agent,
|
||||
onClick = { onAgentSelected(agent) },
|
||||
onLongClick = { showMenu = true }
|
||||
)
|
||||
DropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("记忆管理") },
|
||||
onClick = {
|
||||
showMenu = false
|
||||
onManageMemory(agent.id, agent.name)
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("删除", color = MaterialTheme.colorScheme.error) },
|
||||
onClick = {
|
||||
showMenu = false
|
||||
showDeleteDialog = true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
if (showDeleteDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showDeleteDialog = false },
|
||||
|
||||
@@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material3.*
|
||||
@@ -33,6 +34,8 @@ fun LoginScreen(
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
val focusManager = LocalFocusManager.current
|
||||
var passwordVisible by remember { mutableStateOf(false) }
|
||||
var isEditingUrl by remember { mutableStateOf(false) }
|
||||
var editedUrl by remember { mutableStateOf("") }
|
||||
|
||||
LaunchedEffect(uiState.isLoggedIn) {
|
||||
if (uiState.isLoggedIn) {
|
||||
@@ -68,7 +71,66 @@ fun LoginScreen(
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
Spacer(modifier = Modifier.height(36.dp))
|
||||
|
||||
// Server URL
|
||||
if (isEditingUrl) {
|
||||
OutlinedTextField(
|
||||
value = editedUrl,
|
||||
onValueChange = { editedUrl = it },
|
||||
label = { Text("服务器地址") },
|
||||
placeholder = { Text("http://101.43.95.130:8037") },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
|
||||
enabled = !uiState.isLoading,
|
||||
supportingText = { Text("输入后点击保存,设置即时生效") }
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
TextButton(
|
||||
onClick = { isEditingUrl = false },
|
||||
enabled = !uiState.isLoading
|
||||
) {
|
||||
Text("取消")
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
TextButton(
|
||||
onClick = {
|
||||
viewModel.saveServerUrl(editedUrl)
|
||||
isEditingUrl = false
|
||||
},
|
||||
enabled = !uiState.isLoading
|
||||
) {
|
||||
Text("保存")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
TextButton(
|
||||
onClick = {
|
||||
editedUrl = uiState.serverUrl
|
||||
isEditingUrl = true
|
||||
},
|
||||
enabled = !uiState.isLoading,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Settings,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = if (uiState.serverUrl.isNotBlank()) uiState.serverUrl else "点击配置服务器地址",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Username
|
||||
OutlinedTextField(
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.tiangong.aiagent.ui.login
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.tiangong.aiagent.data.local.TokenDataStore
|
||||
import com.tiangong.aiagent.data.remote.DynamicUrlInterceptor
|
||||
import com.tiangong.aiagent.data.repository.AuthRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -13,6 +15,7 @@ import javax.inject.Inject
|
||||
data class LoginUiState(
|
||||
val username: String = "",
|
||||
val password: String = "",
|
||||
val serverUrl: String = "",
|
||||
val isLoading: Boolean = false,
|
||||
val error: String? = null,
|
||||
val isLoggedIn: Boolean = false
|
||||
@@ -20,12 +23,21 @@ data class LoginUiState(
|
||||
|
||||
@HiltViewModel
|
||||
class LoginViewModel @Inject constructor(
|
||||
private val authRepository: AuthRepository
|
||||
private val authRepository: AuthRepository,
|
||||
private val tokenDataStore: TokenDataStore,
|
||||
private val dynamicUrlInterceptor: DynamicUrlInterceptor
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(LoginUiState())
|
||||
val uiState: StateFlow<LoginUiState> = _uiState.asStateFlow()
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
val url = tokenDataStore.getServerUrl() ?: ""
|
||||
_uiState.value = _uiState.value.copy(serverUrl = url)
|
||||
}
|
||||
}
|
||||
|
||||
fun onUsernameChange(username: String) {
|
||||
_uiState.value = _uiState.value.copy(username = username, error = null)
|
||||
}
|
||||
@@ -34,6 +46,27 @@ class LoginViewModel @Inject constructor(
|
||||
_uiState.value = _uiState.value.copy(password = password, error = null)
|
||||
}
|
||||
|
||||
fun onServerUrlChange(url: String) {
|
||||
_uiState.value = _uiState.value.copy(serverUrl = url, error = null)
|
||||
}
|
||||
|
||||
fun saveServerUrl(url: String) {
|
||||
val trimmed = url.trim().trimEnd('/')
|
||||
if (trimmed.isBlank()) {
|
||||
_uiState.value = _uiState.value.copy(error = "服务器地址不能为空")
|
||||
return
|
||||
}
|
||||
if (!trimmed.startsWith("http://") && !trimmed.startsWith("https://")) {
|
||||
_uiState.value = _uiState.value.copy(error = "服务器地址必须以 http:// 或 https:// 开头")
|
||||
return
|
||||
}
|
||||
viewModelScope.launch {
|
||||
tokenDataStore.saveServerUrl(trimmed)
|
||||
dynamicUrlInterceptor.updateBaseUrl(trimmed)
|
||||
_uiState.value = _uiState.value.copy(serverUrl = trimmed, error = null)
|
||||
}
|
||||
}
|
||||
|
||||
fun login() {
|
||||
val state = _uiState.value
|
||||
if (state.username.isBlank() || state.password.isBlank()) {
|
||||
|
||||
@@ -0,0 +1,631 @@
|
||||
package com.tiangong.aiagent.ui.memory
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.tiangong.aiagent.data.remote.dto.GlobalKnowledgeDto
|
||||
import com.tiangong.aiagent.data.remote.dto.KnowledgeEntityDto
|
||||
import com.tiangong.aiagent.data.remote.dto.LearningPatternDto
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MemoryManageScreen(
|
||||
agentId: String,
|
||||
agentName: String,
|
||||
onBack: () -> Unit,
|
||||
viewModel: MemoryManageViewModel = hiltViewModel()
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
val context = LocalContext.current
|
||||
var showDeleteDialog by remember { mutableStateOf<String?>(null) }
|
||||
var showDeleteEntityDialog by remember { mutableStateOf<String?>(null) }
|
||||
var showDeletePatternDialog by remember { mutableStateOf<String?>(null) }
|
||||
var showDeleteVectorDialog by remember { mutableStateOf<String?>(null) }
|
||||
var editContent by remember { mutableStateOf("") }
|
||||
var editTags by remember { mutableStateOf("") }
|
||||
var editConfidence by remember { mutableStateOf("medium") }
|
||||
var showImportDialog by remember { mutableStateOf(false) }
|
||||
var importJson by remember { mutableStateOf("") }
|
||||
|
||||
// File picker for import
|
||||
val filePicker = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.GetContent()
|
||||
) { uri ->
|
||||
uri?.let {
|
||||
try {
|
||||
val inputStream = context.contentResolver.openInputStream(it)
|
||||
val text = inputStream?.bufferedReader()?.readText() ?: ""
|
||||
inputStream?.close()
|
||||
viewModel.importMemory(text)
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Init
|
||||
LaunchedEffect(agentId) {
|
||||
viewModel.initialize(agentId)
|
||||
}
|
||||
|
||||
// Export share
|
||||
LaunchedEffect(uiState.exportJson) {
|
||||
uiState.exportJson?.let { json ->
|
||||
val sendIntent = Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
putExtra(Intent.EXTRA_TEXT, json)
|
||||
type = "application/json"
|
||||
}
|
||||
context.startActivity(Intent.createChooser(sendIntent, "导出记忆数据"))
|
||||
viewModel.clearExportJson()
|
||||
}
|
||||
}
|
||||
|
||||
// Error / Success snackbar
|
||||
LaunchedEffect(uiState.error, uiState.successMsg) {
|
||||
// auto-dismiss handled by clearMessages
|
||||
}
|
||||
|
||||
// Edit dialog
|
||||
if (uiState.showCreateDialog) {
|
||||
val item = uiState.editingItem
|
||||
LaunchedEffect(item) {
|
||||
editContent = item?.content ?: ""
|
||||
editTags = item?.tags?.joinToString(", ") ?: ""
|
||||
editConfidence = item?.confidence ?: "medium"
|
||||
}
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = { viewModel.dismissDialog() },
|
||||
title = { Text(if (item != null) "编辑知识" else "新增知识") },
|
||||
text = {
|
||||
Column {
|
||||
OutlinedTextField(
|
||||
value = editContent,
|
||||
onValueChange = { editContent = it },
|
||||
label = { Text("内容") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
maxLines = 5
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = editTags,
|
||||
onValueChange = { editTags = it },
|
||||
label = { Text("标签(逗号分隔)") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
// Confidence dropdown
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = { expanded = it }
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = editConfidence,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text("置信度") },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false }
|
||||
) {
|
||||
listOf("low", "medium", "high").forEach { c ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(c) },
|
||||
onClick = {
|
||||
editConfidence = c
|
||||
expanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
val tags = editTags.split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||
viewModel.saveKnowledge(editContent, tags.ifEmpty { null }, editConfidence)
|
||||
},
|
||||
enabled = editContent.isNotBlank()
|
||||
) {
|
||||
Text("保存")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { viewModel.dismissDialog() }) { Text("取消") }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Import dialog
|
||||
if (showImportDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showImportDialog = false },
|
||||
title = { Text("导入记忆") },
|
||||
text = {
|
||||
Column {
|
||||
Text("粘贴 JSON 数据或选择文件", style = MaterialTheme.typography.bodySmall)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = importJson,
|
||||
onValueChange = { importJson = it },
|
||||
label = { Text("JSON 数据") },
|
||||
modifier = Modifier.fillMaxWidth().height(150.dp),
|
||||
maxLines = 8
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
OutlinedButton(onClick = { filePicker.launch("application/json") }) {
|
||||
Icon(Icons.Default.FileOpen, contentDescription = null)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text("选择文件")
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
if (importJson.isNotBlank()) {
|
||||
viewModel.importMemory(importJson)
|
||||
}
|
||||
showImportDialog = false
|
||||
},
|
||||
enabled = importJson.isNotBlank()
|
||||
) { Text("导入") }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showImportDialog = false }) { Text("取消") }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Delete confirmation
|
||||
showDeleteDialog?.let { id ->
|
||||
AlertDialog(
|
||||
onDismissRequest = { showDeleteDialog = null },
|
||||
title = { Text("确认删除") },
|
||||
text = { Text("确定要删除这条知识吗?") },
|
||||
confirmButton = {
|
||||
TextButton(onClick = { viewModel.deleteKnowledge(id); showDeleteDialog = null }) {
|
||||
Text("删除", color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showDeleteDialog = null }) { Text("取消") }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
showDeleteEntityDialog?.let { id ->
|
||||
AlertDialog(
|
||||
onDismissRequest = { showDeleteEntityDialog = null },
|
||||
title = { Text("确认删除") },
|
||||
text = { Text("确定要删除这个知识实体吗?") },
|
||||
confirmButton = {
|
||||
TextButton(onClick = { viewModel.deleteEntity(id); showDeleteEntityDialog = null }) {
|
||||
Text("删除", color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showDeleteEntityDialog = null }) { Text("取消") }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
showDeletePatternDialog?.let { id ->
|
||||
AlertDialog(
|
||||
onDismissRequest = { showDeletePatternDialog = null },
|
||||
title = { Text("确认删除") },
|
||||
text = { Text("确定要删除这个学习模式吗?") },
|
||||
confirmButton = {
|
||||
TextButton(onClick = { viewModel.deletePattern(id); showDeletePatternDialog = null }) {
|
||||
Text("删除", color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showDeletePatternDialog = null }) { Text("取消") }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
showDeleteVectorDialog?.let { id ->
|
||||
AlertDialog(
|
||||
onDismissRequest = { showDeleteVectorDialog = null },
|
||||
title = { Text("确认删除") },
|
||||
text = { Text("确定要删除这条向量记忆吗?") },
|
||||
confirmButton = {
|
||||
TextButton(onClick = { viewModel.deleteVectorMemory(id); showDeleteVectorDialog = null }) {
|
||||
Text("删除", color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showDeleteVectorDialog = null }) { Text("取消") }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("${agentName} 记忆管理") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "返回")
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
// Import
|
||||
IconButton(onClick = { showImportDialog = true }) {
|
||||
Icon(Icons.Default.FileOpen, contentDescription = "导入")
|
||||
}
|
||||
// Export
|
||||
IconButton(
|
||||
onClick = { viewModel.exportMemory() },
|
||||
enabled = !uiState.isExporting
|
||||
) {
|
||||
if (uiState.isExporting) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(20.dp), strokeWidth = 2.dp)
|
||||
} else {
|
||||
Icon(Icons.Default.Share, contentDescription = "导出")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Box(modifier = Modifier.fillMaxSize().padding(paddingValues)) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
// Tabs
|
||||
TabRow(selectedTabIndex = uiState.selectedTab) {
|
||||
Tab(
|
||||
selected = uiState.selectedTab == 0,
|
||||
onClick = { viewModel.onTabSelected(0) },
|
||||
text = { Text("全局知识 (${uiState.totalKnowledge})") }
|
||||
)
|
||||
Tab(
|
||||
selected = uiState.selectedTab == 1,
|
||||
onClick = { viewModel.onTabSelected(1) },
|
||||
text = { Text("知识实体 (${uiState.knowledgeEntities.size})") }
|
||||
)
|
||||
Tab(
|
||||
selected = uiState.selectedTab == 2,
|
||||
onClick = { viewModel.onTabSelected(2) },
|
||||
text = { Text("学习模式 (${uiState.totalPatterns})") }
|
||||
)
|
||||
Tab(
|
||||
selected = uiState.selectedTab == 3,
|
||||
onClick = { viewModel.onTabSelected(3) },
|
||||
text = { Text("对话记忆 (${uiState.totalVectorMemories})") }
|
||||
)
|
||||
}
|
||||
|
||||
// Search
|
||||
OutlinedTextField(
|
||||
value = uiState.searchQuery,
|
||||
onValueChange = viewModel::onSearchChange,
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
placeholder = { Text("搜索...") },
|
||||
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
// Error & success
|
||||
uiState.error?.let { err ->
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 4.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer)
|
||||
) {
|
||||
Row(modifier = Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(err, modifier = Modifier.weight(1f), style = MaterialTheme.typography.bodySmall)
|
||||
TextButton(onClick = { viewModel.clearMessages() }) { Text("关闭") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uiState.successMsg?.let { msg ->
|
||||
LaunchedEffect(msg) {
|
||||
kotlinx.coroutines.delay(2000)
|
||||
viewModel.clearMessages()
|
||||
}
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 4.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer)
|
||||
) {
|
||||
Text(msg, modifier = Modifier.padding(12.dp), style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
}
|
||||
|
||||
// Content
|
||||
if (uiState.isLoading) {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
} else {
|
||||
when (uiState.selectedTab) {
|
||||
0 -> GlobalKnowledgeTab(
|
||||
items = uiState.globalKnowledge,
|
||||
onEdit = { viewModel.showEditDialog(it) },
|
||||
onDelete = { showDeleteDialog = it }
|
||||
)
|
||||
1 -> KnowledgeEntitiesTab(
|
||||
items = uiState.knowledgeEntities,
|
||||
onDelete = { showDeleteEntityDialog = it }
|
||||
)
|
||||
2 -> LearningPatternsTab(
|
||||
items = uiState.learningPatterns,
|
||||
onDelete = { showDeletePatternDialog = it }
|
||||
)
|
||||
3 -> VectorMemoriesTab(
|
||||
items = uiState.vectorMemories,
|
||||
onDelete = { showDeleteVectorDialog = it }
|
||||
)
|
||||
}
|
||||
}
|
||||
} // end Column
|
||||
|
||||
// FAB for add knowledge
|
||||
if (uiState.selectedTab == 0) {
|
||||
FloatingActionButton(
|
||||
onClick = { viewModel.showCreateDialog() },
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(16.dp),
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer
|
||||
) {
|
||||
Icon(Icons.Default.Add, contentDescription = "新增知识")
|
||||
}
|
||||
}
|
||||
} // end Box
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Tab Content ───
|
||||
|
||||
@Composable
|
||||
private fun GlobalKnowledgeTab(
|
||||
items: List<GlobalKnowledgeDto>,
|
||||
onEdit: (GlobalKnowledgeDto) -> Unit,
|
||||
onDelete: (String) -> Unit
|
||||
) {
|
||||
if (items.isEmpty()) {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text("暂无全局知识", color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp)
|
||||
) {
|
||||
items(items, key = { it.id }) { item ->
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
Text(
|
||||
text = item.content,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 4,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
item.tags?.take(3)?.forEach { tag ->
|
||||
SuggestionChip(
|
||||
onClick = {},
|
||||
label = { Text(tag, style = MaterialTheme.typography.labelSmall) }
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = "置信度: ${item.confidence}",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
Row {
|
||||
IconButton(onClick = { onEdit(item) }, modifier = Modifier.size(32.dp)) {
|
||||
Icon(Icons.Default.Edit, contentDescription = "编辑", modifier = Modifier.size(16.dp))
|
||||
}
|
||||
IconButton(onClick = { onDelete(item.id) }, modifier = Modifier.size(32.dp)) {
|
||||
Icon(Icons.Default.Delete, contentDescription = "删除", modifier = Modifier.size(16.dp), tint = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun KnowledgeEntitiesTab(
|
||||
items: List<KnowledgeEntityDto>,
|
||||
onDelete: (String) -> Unit
|
||||
) {
|
||||
if (items.isEmpty()) {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text("暂无知识实体", color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp)
|
||||
) {
|
||||
items(items, key = { it.id }) { item ->
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = item.name,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
text = "${item.entityType} · ${item.source} · ${item.confidence}",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
item.description?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
IconButton(onClick = { onDelete(item.id) }) {
|
||||
Icon(Icons.Default.Delete, contentDescription = "删除", tint = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LearningPatternsTab(
|
||||
items: List<LearningPatternDto>,
|
||||
onDelete: (String) -> Unit
|
||||
) {
|
||||
if (items.isEmpty()) {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text("暂无学习模式", color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp)
|
||||
) {
|
||||
items(items, key = { it.id }) { item ->
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
Text(
|
||||
text = item.taskCategory,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(
|
||||
text = "成功率: ${(item.effectivenessScore * 100).toInt()}% · " +
|
||||
"运行: ${item.totalRuns}次 · " +
|
||||
"平均${item.avgIterations}轮/${item.avgToolCalls}工具",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
item.taskKeywords?.let {
|
||||
Text(text = "关键词: $it", style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
item.suggestedTools?.let {
|
||||
Text(
|
||||
text = "工具: $it",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
IconButton(onClick = { onDelete(item.id) }) {
|
||||
Icon(Icons.Default.Delete, contentDescription = "删除", modifier = Modifier.size(16.dp), tint = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VectorMemoriesTab(
|
||||
items: List<com.tiangong.aiagent.data.remote.dto.VectorMemoryDto>,
|
||||
onDelete: (String) -> Unit
|
||||
) {
|
||||
if (items.isEmpty()) {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text("暂无对话记忆", color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp)
|
||||
) {
|
||||
items(items, key = { it.id }) { item ->
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
Text(
|
||||
text = item.contentText,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = 5,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
item.metadata?.get("memory_type")?.let { type ->
|
||||
SuggestionChip(
|
||||
onClick = {},
|
||||
label = { Text(type.toString(), style = MaterialTheme.typography.labelSmall) }
|
||||
)
|
||||
}
|
||||
item.createdAt?.let { date ->
|
||||
Text(
|
||||
text = date.take(16).replace("T", " "),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
IconButton(onClick = { onDelete(item.id) }, modifier = Modifier.size(32.dp)) {
|
||||
Icon(Icons.Default.Delete, contentDescription = "删除", modifier = Modifier.size(16.dp), tint = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
package com.tiangong.aiagent.ui.memory
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.tiangong.aiagent.data.remote.ApiService
|
||||
import com.tiangong.aiagent.data.remote.dto.*
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
data class MemoryUiState(
|
||||
val isLoading: Boolean = false,
|
||||
val globalKnowledge: List<GlobalKnowledgeDto> = emptyList(),
|
||||
val knowledgeEntities: List<KnowledgeEntityDto> = emptyList(),
|
||||
val learningPatterns: List<LearningPatternDto> = emptyList(),
|
||||
val vectorMemories: List<VectorMemoryDto> = emptyList(),
|
||||
val totalKnowledge: Int = 0,
|
||||
val totalPatterns: Int = 0,
|
||||
val totalVectorMemories: Int = 0,
|
||||
val searchQuery: String = "",
|
||||
val selectedTab: Int = 0,
|
||||
val isExporting: Boolean = false,
|
||||
val exportJson: String? = null,
|
||||
val error: String? = null,
|
||||
val successMsg: String? = null,
|
||||
val editingItem: GlobalKnowledgeDto? = null,
|
||||
val showCreateDialog: Boolean = false
|
||||
)
|
||||
|
||||
@HiltViewModel
|
||||
class MemoryManageViewModel @Inject constructor(
|
||||
private val apiService: ApiService
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(MemoryUiState())
|
||||
val uiState: StateFlow<MemoryUiState> = _uiState.asStateFlow()
|
||||
|
||||
private var agentId: String = ""
|
||||
|
||||
fun initialize(agentId: String) {
|
||||
if (this.agentId == agentId) return
|
||||
this.agentId = agentId
|
||||
loadAll()
|
||||
}
|
||||
|
||||
fun loadAll() {
|
||||
viewModelScope.launch {
|
||||
_uiState.value = _uiState.value.copy(isLoading = true)
|
||||
try {
|
||||
val gk = apiService.getGlobalKnowledge(agentId)
|
||||
val ke = apiService.getKnowledgeEntities(agentId)
|
||||
val lp = apiService.getLearningPatterns(agentId)
|
||||
val vm = apiService.getVectorMemories(agentId)
|
||||
_uiState.value = _uiState.value.copy(
|
||||
globalKnowledge = gk.items,
|
||||
knowledgeEntities = ke.items,
|
||||
learningPatterns = lp.items,
|
||||
vectorMemories = vm.items,
|
||||
totalKnowledge = gk.total,
|
||||
totalPatterns = lp.total,
|
||||
totalVectorMemories = vm.total,
|
||||
isLoading = false
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.w("MemoryVM", "Failed to load memory", e)
|
||||
_uiState.value = _uiState.value.copy(isLoading = false, error = "加载失败: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onSearchChange(query: String) {
|
||||
_uiState.value = _uiState.value.copy(searchQuery = query)
|
||||
if (agentId.isEmpty()) return
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val gk = apiService.getGlobalKnowledge(agentId, search = query.ifBlank { null })
|
||||
_uiState.value = _uiState.value.copy(
|
||||
globalKnowledge = gk.items,
|
||||
totalKnowledge = gk.total
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.w("MemoryVM", "Search failed", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onTabSelected(index: Int) {
|
||||
_uiState.value = _uiState.value.copy(selectedTab = index)
|
||||
}
|
||||
|
||||
// ─── Global Knowledge CRUD ───
|
||||
|
||||
fun showCreateDialog() {
|
||||
_uiState.value = _uiState.value.copy(showCreateDialog = true, editingItem = null)
|
||||
}
|
||||
|
||||
fun showEditDialog(item: GlobalKnowledgeDto) {
|
||||
_uiState.value = _uiState.value.copy(showCreateDialog = true, editingItem = item)
|
||||
}
|
||||
|
||||
fun dismissDialog() {
|
||||
_uiState.value = _uiState.value.copy(showCreateDialog = false, editingItem = null)
|
||||
}
|
||||
|
||||
fun saveKnowledge(content: String, tags: List<String>?, confidence: String) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val editing = _uiState.value.editingItem
|
||||
if (editing != null) {
|
||||
apiService.updateGlobalKnowledge(agentId, editing.id,
|
||||
GlobalKnowledgeCreateRequest(content, tags, confidence))
|
||||
} else {
|
||||
apiService.createGlobalKnowledge(agentId,
|
||||
GlobalKnowledgeCreateRequest(content, tags, confidence))
|
||||
}
|
||||
_uiState.value = _uiState.value.copy(showCreateDialog = false, editingItem = null, successMsg = "保存成功")
|
||||
loadAll()
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = _uiState.value.copy(error = "保存失败: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteKnowledge(id: String) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
apiService.deleteGlobalKnowledge(agentId, id)
|
||||
_uiState.value = _uiState.value.copy(successMsg = "已删除")
|
||||
loadAll()
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = _uiState.value.copy(error = "删除失败: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteEntity(id: String) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
apiService.deleteKnowledgeEntity(agentId, id)
|
||||
_uiState.value = _uiState.value.copy(successMsg = "已删除")
|
||||
loadAll()
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = _uiState.value.copy(error = "删除失败: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deletePattern(id: String) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
apiService.deleteLearningPattern(agentId, id)
|
||||
_uiState.value = _uiState.value.copy(successMsg = "已删除")
|
||||
loadAll()
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = _uiState.value.copy(error = "删除失败: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteVectorMemory(id: String) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
apiService.deleteVectorMemory(agentId, id)
|
||||
_uiState.value = _uiState.value.copy(successMsg = "已删除")
|
||||
loadAll()
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = _uiState.value.copy(error = "删除失败: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Import / Export ───
|
||||
|
||||
fun exportMemory() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
_uiState.value = _uiState.value.copy(isExporting = true)
|
||||
val response = apiService.exportMemory(agentId)
|
||||
val gson = com.google.gson.GsonBuilder().setPrettyPrinting().create()
|
||||
val json = gson.toJson(response)
|
||||
_uiState.value = _uiState.value.copy(isExporting = false, exportJson = json, successMsg = "导出成功")
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = _uiState.value.copy(isExporting = false, error = "导出失败: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun importMemory(json: String) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val gson = com.google.gson.Gson()
|
||||
val response = gson.fromJson(json, MemoryExportResponse::class.java)
|
||||
val request = MemoryImportRequest(
|
||||
globalKnowledge = response.globalKnowledge,
|
||||
knowledgeEntities = response.knowledgeEntities,
|
||||
knowledgeRelations = response.knowledgeRelations,
|
||||
learningPatterns = response.learningPatterns,
|
||||
vectorMemories = response.vectorMemories
|
||||
)
|
||||
apiService.importMemory(agentId, request)
|
||||
_uiState.value = _uiState.value.copy(successMsg = "导入成功")
|
||||
loadAll()
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = _uiState.value.copy(error = "导入失败: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearExportJson() {
|
||||
_uiState.value = _uiState.value.copy(exportJson = null)
|
||||
}
|
||||
|
||||
fun clearMessages() {
|
||||
_uiState.value = _uiState.value.copy(error = null, successMsg = null)
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import com.tiangong.aiagent.ui.chat.ChatScreen
|
||||
import com.tiangong.aiagent.ui.chat.ChatViewModel
|
||||
import com.tiangong.aiagent.ui.history.ConversationListScreen
|
||||
import com.tiangong.aiagent.ui.login.LoginScreen
|
||||
import com.tiangong.aiagent.ui.memory.MemoryManageScreen
|
||||
import com.tiangong.aiagent.ui.notifications.NotificationDetailScreen
|
||||
import com.tiangong.aiagent.ui.notifications.NotificationsScreen
|
||||
import com.tiangong.aiagent.ui.register.RegisterScreen
|
||||
@@ -32,6 +33,7 @@ object Routes {
|
||||
const val ABOUT = "about"
|
||||
const val NOTIFICATIONS = "notifications"
|
||||
const val NOTIFICATION_DETAIL = "notifications/{notificationId}"
|
||||
const val MEMORY_MANAGE = "memory_manage/{agentId}/{agentName}"
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -104,6 +106,10 @@ fun NavGraph(
|
||||
onAgentSelected = { agent: Agent ->
|
||||
chatViewModel.switchAgent(agent)
|
||||
navController.popBackStack()
|
||||
},
|
||||
onManageMemory = { agentId, agentName ->
|
||||
val encoded = java.net.URLEncoder.encode(agentName, "UTF-8")
|
||||
navController.navigate("memory_manage/$agentId/$encoded")
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -155,5 +161,17 @@ fun NavGraph(
|
||||
navController = navController
|
||||
)
|
||||
}
|
||||
|
||||
composable(Routes.MEMORY_MANAGE) { backStackEntry ->
|
||||
val agentId = backStackEntry.arguments?.getString("agentId") ?: return@composable
|
||||
val agentName = java.net.URLDecoder.decode(
|
||||
backStackEntry.arguments?.getString("agentName") ?: "", "UTF-8"
|
||||
)
|
||||
MemoryManageScreen(
|
||||
agentId = agentId,
|
||||
agentName = agentName,
|
||||
onBack = { navController.popBackStack() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
680
backend/app/api/agent_memory.py
Normal file
680
backend/app/api/agent_memory.py
Normal file
@@ -0,0 +1,680 @@
|
||||
"""
|
||||
Agent 记忆管理 API — 全局知识 / 知识实体 / 学习模式的 CRUD + 导入导出
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import or_
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.api.auth import get_current_user
|
||||
from app.models.user import User
|
||||
from app.models.agent import Agent, GlobalKnowledge, KnowledgeEntity, KnowledgeRelation
|
||||
from app.models.agent_learning_pattern import AgentLearningPattern
|
||||
from app.models.agent_vector_memory import AgentVectorMemory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/v1/agents",
|
||||
tags=["agent-memory"],
|
||||
)
|
||||
|
||||
SCOPE_AGENT = "agent"
|
||||
|
||||
|
||||
def _check_agent(db: Session, agent_id: str, user: User):
|
||||
agent = db.query(Agent).filter(Agent.id == agent_id).first()
|
||||
if not agent:
|
||||
raise HTTPException(status_code=404, detail="Agent 不存在")
|
||||
if agent.user_id and agent.user_id != user.id and user.role != "admin":
|
||||
raise HTTPException(status_code=403, detail="无权访问该 Agent")
|
||||
return agent
|
||||
|
||||
|
||||
# ─────────── Pydantic models ───────────
|
||||
|
||||
|
||||
class GlobalKnowledgeItem(BaseModel):
|
||||
id: str
|
||||
content: str
|
||||
source_agent_id: Optional[str] = None
|
||||
source_user_id: Optional[str] = None
|
||||
tags: Optional[list] = None
|
||||
confidence: str = "medium"
|
||||
scope_kind: str = "agent"
|
||||
scope_id: str = ""
|
||||
expires_at: Optional[str] = None
|
||||
created_at: Optional[str] = None
|
||||
|
||||
|
||||
class GlobalKnowledgeCreate(BaseModel):
|
||||
content: str
|
||||
tags: Optional[list] = None
|
||||
confidence: str = "medium"
|
||||
|
||||
|
||||
class GlobalKnowledgeUpdate(BaseModel):
|
||||
content: Optional[str] = None
|
||||
tags: Optional[list] = None
|
||||
confidence: Optional[str] = None
|
||||
|
||||
|
||||
class GlobalKnowledgeList(BaseModel):
|
||||
items: List[GlobalKnowledgeItem]
|
||||
total: int
|
||||
|
||||
|
||||
class KnowledgeEntityItem(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
entity_type: str = "concept"
|
||||
description: Optional[str] = None
|
||||
source: str = "extracted"
|
||||
confidence: str = "medium"
|
||||
tags: Optional[list] = None
|
||||
scope_kind: str = "agent"
|
||||
scope_id: str = ""
|
||||
created_at: Optional[str] = None
|
||||
|
||||
|
||||
class KnowledgeEntityCreate(BaseModel):
|
||||
name: str
|
||||
entity_type: str = "concept"
|
||||
description: Optional[str] = None
|
||||
tags: Optional[list] = None
|
||||
confidence: str = "medium"
|
||||
|
||||
|
||||
class KnowledgeEntityUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
entity_type: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
tags: Optional[list] = None
|
||||
confidence: Optional[str] = None
|
||||
|
||||
|
||||
class KnowledgeEntityList(BaseModel):
|
||||
items: List[KnowledgeEntityItem]
|
||||
total: int
|
||||
|
||||
|
||||
class LearningPatternItem(BaseModel):
|
||||
id: str
|
||||
scope_kind: str
|
||||
scope_id: str
|
||||
task_category: str
|
||||
task_keywords: Optional[str] = None
|
||||
suggested_tools: Optional[str] = None
|
||||
effectiveness_score: float = 0.0
|
||||
total_runs: int = 0
|
||||
successful_runs: int = 0
|
||||
avg_iterations: float = 0.0
|
||||
avg_tool_calls: float = 0.0
|
||||
last_used_at: Optional[str] = None
|
||||
created_at: Optional[str] = None
|
||||
|
||||
|
||||
class LearningPatternList(BaseModel):
|
||||
items: List[LearningPatternItem]
|
||||
total: int
|
||||
|
||||
|
||||
class VectorMemoryItem(BaseModel):
|
||||
id: str
|
||||
scope_kind: str = "agent"
|
||||
scope_id: str = ""
|
||||
session_key: str = ""
|
||||
content_text: str
|
||||
metadata: Optional[dict] = None
|
||||
created_at: Optional[str] = None
|
||||
|
||||
|
||||
class VectorMemoryList(BaseModel):
|
||||
items: List[VectorMemoryItem]
|
||||
total: int
|
||||
|
||||
|
||||
class MemoryExportResponse(BaseModel):
|
||||
agent_id: str
|
||||
exported_at: str
|
||||
global_knowledge: List[dict] = []
|
||||
knowledge_entities: List[dict] = []
|
||||
knowledge_relations: List[dict] = []
|
||||
learning_patterns: List[dict] = []
|
||||
vector_memories: List[dict] = []
|
||||
|
||||
|
||||
class MemoryImportRequest(BaseModel):
|
||||
global_knowledge: List[dict] = []
|
||||
knowledge_entities: List[dict] = []
|
||||
knowledge_relations: List[dict] = []
|
||||
learning_patterns: List[dict] = []
|
||||
vector_memories: List[dict] = []
|
||||
|
||||
|
||||
# ─────────── 辅助 ───────────
|
||||
|
||||
|
||||
def _gk_to_item(gk: GlobalKnowledge) -> GlobalKnowledgeItem:
|
||||
return GlobalKnowledgeItem(
|
||||
id=gk.id,
|
||||
content=gk.content,
|
||||
source_agent_id=gk.source_agent_id,
|
||||
source_user_id=gk.source_user_id,
|
||||
tags=gk.tags,
|
||||
confidence=gk.confidence or "medium",
|
||||
scope_kind=gk.scope_kind or SCOPE_AGENT,
|
||||
scope_id=gk.scope_id or "",
|
||||
expires_at=gk.expires_at.isoformat() if gk.expires_at else None,
|
||||
created_at=gk.created_at.isoformat() if gk.created_at else None,
|
||||
)
|
||||
|
||||
|
||||
def _ke_to_item(ke: KnowledgeEntity) -> KnowledgeEntityItem:
|
||||
return KnowledgeEntityItem(
|
||||
id=ke.id,
|
||||
name=ke.name,
|
||||
entity_type=ke.entity_type or "concept",
|
||||
description=getattr(ke, "description", None),
|
||||
source=ke.source or "extracted",
|
||||
confidence=ke.confidence or "medium",
|
||||
tags=getattr(ke, "metadata_", None),
|
||||
scope_kind=ke.scope_kind or SCOPE_AGENT,
|
||||
scope_id=ke.scope_id or "",
|
||||
created_at=ke.created_at.isoformat() if ke.created_at else None,
|
||||
)
|
||||
|
||||
|
||||
def _vm_to_item(vm: AgentVectorMemory) -> VectorMemoryItem:
|
||||
return VectorMemoryItem(
|
||||
id=vm.id,
|
||||
scope_kind=vm.scope_kind or SCOPE_AGENT,
|
||||
scope_id=vm.scope_id or "",
|
||||
session_key=vm.session_key or "",
|
||||
content_text=vm.content_text or "",
|
||||
metadata=vm.metadata_ or {},
|
||||
created_at=vm.created_at.isoformat() if vm.created_at else None,
|
||||
)
|
||||
|
||||
|
||||
# ─────────── Global Knowledge CRUD ───────────
|
||||
|
||||
|
||||
@router.get("/{agent_id}/memory/global-knowledge", response_model=GlobalKnowledgeList)
|
||||
def list_global_knowledge(
|
||||
agent_id: str,
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
search: Optional[str] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
_check_agent(db, agent_id, current_user)
|
||||
q = db.query(GlobalKnowledge).filter(
|
||||
GlobalKnowledge.scope_kind == SCOPE_AGENT,
|
||||
GlobalKnowledge.scope_id == agent_id,
|
||||
)
|
||||
if search:
|
||||
q = q.filter(GlobalKnowledge.content.contains(search))
|
||||
total = q.count()
|
||||
items = q.order_by(GlobalKnowledge.created_at.desc()).offset(skip).limit(limit).all()
|
||||
return GlobalKnowledgeList(items=[_gk_to_item(it) for it in items], total=total)
|
||||
|
||||
|
||||
@router.post("/{agent_id}/memory/global-knowledge", response_model=GlobalKnowledgeItem)
|
||||
def create_global_knowledge(
|
||||
agent_id: str,
|
||||
body: GlobalKnowledgeCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
_check_agent(db, agent_id, current_user)
|
||||
gk = GlobalKnowledge(
|
||||
id=str(uuid.uuid4()),
|
||||
content=body.content,
|
||||
tags=body.tags,
|
||||
confidence=body.confidence,
|
||||
scope_kind=SCOPE_AGENT,
|
||||
scope_id=agent_id,
|
||||
source_user_id=current_user.id,
|
||||
)
|
||||
db.add(gk)
|
||||
db.commit()
|
||||
db.refresh(gk)
|
||||
return _gk_to_item(gk)
|
||||
|
||||
|
||||
@router.put("/{agent_id}/memory/global-knowledge/{knowledge_id}", response_model=GlobalKnowledgeItem)
|
||||
def update_global_knowledge(
|
||||
agent_id: str,
|
||||
knowledge_id: str,
|
||||
body: GlobalKnowledgeUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
_check_agent(db, agent_id, current_user)
|
||||
gk = db.query(GlobalKnowledge).filter(
|
||||
GlobalKnowledge.id == knowledge_id,
|
||||
GlobalKnowledge.scope_kind == SCOPE_AGENT,
|
||||
GlobalKnowledge.scope_id == agent_id,
|
||||
).first()
|
||||
if not gk:
|
||||
raise HTTPException(status_code=404, detail="知识条目不存在")
|
||||
if body.content is not None:
|
||||
gk.content = body.content
|
||||
if body.tags is not None:
|
||||
gk.tags = body.tags
|
||||
if body.confidence is not None:
|
||||
gk.confidence = body.confidence
|
||||
db.commit()
|
||||
db.refresh(gk)
|
||||
return _gk_to_item(gk)
|
||||
|
||||
|
||||
@router.delete("/{agent_id}/memory/global-knowledge/{knowledge_id}")
|
||||
def delete_global_knowledge(
|
||||
agent_id: str,
|
||||
knowledge_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
_check_agent(db, agent_id, current_user)
|
||||
gk = db.query(GlobalKnowledge).filter(
|
||||
GlobalKnowledge.id == knowledge_id,
|
||||
GlobalKnowledge.scope_kind == SCOPE_AGENT,
|
||||
GlobalKnowledge.scope_id == agent_id,
|
||||
).first()
|
||||
if not gk:
|
||||
raise HTTPException(status_code=404, detail="知识条目不存在")
|
||||
db.delete(gk)
|
||||
db.commit()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# ─────────── Knowledge Entities CRUD ───────────
|
||||
|
||||
|
||||
@router.get("/{agent_id}/memory/knowledge-entities", response_model=KnowledgeEntityList)
|
||||
def list_knowledge_entities(
|
||||
agent_id: str,
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
search: Optional[str] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
_check_agent(db, agent_id, current_user)
|
||||
q = db.query(KnowledgeEntity).filter(
|
||||
KnowledgeEntity.scope_kind == SCOPE_AGENT,
|
||||
KnowledgeEntity.scope_id == agent_id,
|
||||
)
|
||||
if search:
|
||||
q = q.filter(
|
||||
(KnowledgeEntity.name.contains(search))
|
||||
| (KnowledgeEntity.description.contains(search))
|
||||
)
|
||||
total = q.count()
|
||||
items = q.order_by(KnowledgeEntity.created_at.desc()).offset(skip).limit(limit).all()
|
||||
return KnowledgeEntityList(items=[_ke_to_item(it) for it in items], total=total)
|
||||
|
||||
|
||||
@router.post("/{agent_id}/memory/knowledge-entities", response_model=KnowledgeEntityItem)
|
||||
def create_knowledge_entity(
|
||||
agent_id: str,
|
||||
body: KnowledgeEntityCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
_check_agent(db, agent_id, current_user)
|
||||
ke = KnowledgeEntity(
|
||||
id=str(uuid.uuid4()),
|
||||
name=body.name,
|
||||
entity_type=body.entity_type,
|
||||
description=body.description,
|
||||
source="manual",
|
||||
confidence=body.confidence,
|
||||
metadata_=body.tags,
|
||||
scope_kind=SCOPE_AGENT,
|
||||
scope_id=agent_id,
|
||||
user_id=current_user.id,
|
||||
)
|
||||
db.add(ke)
|
||||
db.commit()
|
||||
db.refresh(ke)
|
||||
return _ke_to_item(ke)
|
||||
|
||||
|
||||
@router.put("/{agent_id}/memory/knowledge-entities/{entity_id}", response_model=KnowledgeEntityItem)
|
||||
def update_knowledge_entity(
|
||||
agent_id: str,
|
||||
entity_id: str,
|
||||
body: KnowledgeEntityUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
_check_agent(db, agent_id, current_user)
|
||||
ke = db.query(KnowledgeEntity).filter(
|
||||
KnowledgeEntity.id == entity_id,
|
||||
KnowledgeEntity.scope_kind == SCOPE_AGENT,
|
||||
KnowledgeEntity.scope_id == agent_id,
|
||||
).first()
|
||||
if not ke:
|
||||
raise HTTPException(status_code=404, detail="实体不存在")
|
||||
if body.name is not None:
|
||||
ke.name = body.name
|
||||
if body.entity_type is not None:
|
||||
ke.entity_type = body.entity_type
|
||||
if body.description is not None:
|
||||
ke.description = body.description
|
||||
if body.tags is not None:
|
||||
ke.metadata_ = body.tags
|
||||
if body.confidence is not None:
|
||||
ke.confidence = body.confidence
|
||||
db.commit()
|
||||
db.refresh(ke)
|
||||
return _ke_to_item(ke)
|
||||
|
||||
|
||||
@router.delete("/{agent_id}/memory/knowledge-entities/{entity_id}")
|
||||
def delete_knowledge_entity(
|
||||
agent_id: str,
|
||||
entity_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
_check_agent(db, agent_id, current_user)
|
||||
ke = db.query(KnowledgeEntity).filter(
|
||||
KnowledgeEntity.id == entity_id,
|
||||
KnowledgeEntity.scope_kind == SCOPE_AGENT,
|
||||
KnowledgeEntity.scope_id == agent_id,
|
||||
).first()
|
||||
if not ke:
|
||||
raise HTTPException(status_code=404, detail="实体不存在")
|
||||
# Also remove related relations
|
||||
db.query(KnowledgeRelation).filter(
|
||||
(KnowledgeRelation.source_entity_id == entity_id)
|
||||
| (KnowledgeRelation.target_entity_id == entity_id)
|
||||
).delete()
|
||||
db.delete(ke)
|
||||
db.commit()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# ─────────── Learning Patterns ───────────
|
||||
|
||||
|
||||
@router.get("/{agent_id}/memory/learning-patterns", response_model=LearningPatternList)
|
||||
def list_learning_patterns(
|
||||
agent_id: str,
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
_check_agent(db, agent_id, current_user)
|
||||
q = db.query(AgentLearningPattern).filter(
|
||||
AgentLearningPattern.scope_kind == SCOPE_AGENT,
|
||||
AgentLearningPattern.scope_id == agent_id,
|
||||
)
|
||||
total = q.count()
|
||||
items = q.order_by(AgentLearningPattern.updated_at.desc()).offset(skip).limit(limit).all()
|
||||
return LearningPatternList(
|
||||
items=[
|
||||
LearningPatternItem(
|
||||
id=lp.id,
|
||||
scope_kind=lp.scope_kind or SCOPE_AGENT,
|
||||
scope_id=lp.scope_id or "",
|
||||
task_category=lp.task_category or "",
|
||||
task_keywords=lp.task_keywords,
|
||||
suggested_tools=lp.suggested_tools,
|
||||
effectiveness_score=lp.effectiveness_score or 0.0,
|
||||
total_runs=lp.total_runs or 0,
|
||||
successful_runs=lp.successful_runs or 0,
|
||||
avg_iterations=lp.avg_iterations or 0.0,
|
||||
avg_tool_calls=lp.avg_tool_calls or 0.0,
|
||||
last_used_at=lp.last_used_at.isoformat() if lp.last_used_at else None,
|
||||
created_at=lp.created_at.isoformat() if lp.created_at else None,
|
||||
)
|
||||
for lp in items
|
||||
],
|
||||
total=total,
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{agent_id}/memory/learning-patterns/{pattern_id}")
|
||||
def delete_learning_pattern(
|
||||
agent_id: str,
|
||||
pattern_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
_check_agent(db, agent_id, current_user)
|
||||
lp = db.query(AgentLearningPattern).filter(
|
||||
AgentLearningPattern.id == pattern_id,
|
||||
AgentLearningPattern.scope_kind == SCOPE_AGENT,
|
||||
AgentLearningPattern.scope_id == agent_id,
|
||||
).first()
|
||||
if not lp:
|
||||
raise HTTPException(status_code=404, detail="学习模式不存在")
|
||||
db.delete(lp)
|
||||
db.commit()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# ─────────── Vector Memories ───────────
|
||||
|
||||
|
||||
@router.get("/{agent_id}/memory/vector-memories", response_model=VectorMemoryList)
|
||||
def list_vector_memories(
|
||||
agent_id: str,
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
search: Optional[str] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
_check_agent(db, agent_id, current_user)
|
||||
uid = current_user.id
|
||||
# scope_id may be: agent_id, or {user_id}:{agent_id}, or {user_id}:{agent_id}:{session_id}
|
||||
q = db.query(AgentVectorMemory).filter(
|
||||
AgentVectorMemory.scope_kind == SCOPE_AGENT,
|
||||
or_(
|
||||
AgentVectorMemory.scope_id == agent_id,
|
||||
AgentVectorMemory.scope_id.like(f"%:{agent_id}"),
|
||||
AgentVectorMemory.scope_id.like(f"{uid}:%"),
|
||||
),
|
||||
)
|
||||
if search:
|
||||
q = q.filter(AgentVectorMemory.content_text.contains(search))
|
||||
total = q.count()
|
||||
items = q.order_by(AgentVectorMemory.created_at.desc()).offset(skip).limit(limit).all()
|
||||
return VectorMemoryList(items=[_vm_to_item(it) for it in items], total=total)
|
||||
|
||||
|
||||
@router.delete("/{agent_id}/memory/vector-memories/{memory_id}")
|
||||
def delete_vector_memory(
|
||||
agent_id: str,
|
||||
memory_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
_check_agent(db, agent_id, current_user)
|
||||
uid = current_user.id
|
||||
vm = (
|
||||
db.query(AgentVectorMemory)
|
||||
.filter(
|
||||
AgentVectorMemory.id == memory_id,
|
||||
AgentVectorMemory.scope_kind == SCOPE_AGENT,
|
||||
or_(
|
||||
AgentVectorMemory.scope_id == agent_id,
|
||||
AgentVectorMemory.scope_id.like(f"%:{agent_id}"),
|
||||
AgentVectorMemory.scope_id.like(f"{uid}:%"),
|
||||
),
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not vm:
|
||||
raise HTTPException(status_code=404, detail="向量记忆不存在")
|
||||
db.delete(vm)
|
||||
db.commit()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# ─────────── Import / Export ───────────
|
||||
|
||||
|
||||
def _serialize(row):
|
||||
"""Serialize a SQLAlchemy row to dict, handling datetime."""
|
||||
d = {}
|
||||
for c in row.__table__.columns:
|
||||
val = getattr(row, c.name)
|
||||
if isinstance(val, datetime):
|
||||
val = val.isoformat()
|
||||
d[c.name] = val
|
||||
return d
|
||||
|
||||
|
||||
@router.post("/{agent_id}/memory/export", response_model=MemoryExportResponse)
|
||||
def export_memory(
|
||||
agent_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
_check_agent(db, agent_id, current_user)
|
||||
gk_list = (
|
||||
db.query(GlobalKnowledge)
|
||||
.filter(GlobalKnowledge.scope_kind == SCOPE_AGENT, GlobalKnowledge.scope_id == agent_id)
|
||||
.all()
|
||||
)
|
||||
ke_list = (
|
||||
db.query(KnowledgeEntity)
|
||||
.filter(KnowledgeEntity.scope_kind == SCOPE_AGENT, KnowledgeEntity.scope_id == agent_id)
|
||||
.all()
|
||||
)
|
||||
kr_list = (
|
||||
db.query(KnowledgeRelation)
|
||||
.filter(KnowledgeRelation.scope_kind == SCOPE_AGENT, KnowledgeRelation.scope_id == agent_id)
|
||||
.all()
|
||||
)
|
||||
lp_list = (
|
||||
db.query(AgentLearningPattern)
|
||||
.filter(AgentLearningPattern.scope_kind == SCOPE_AGENT, AgentLearningPattern.scope_id == agent_id)
|
||||
.all()
|
||||
)
|
||||
vm_list = (
|
||||
db.query(AgentVectorMemory)
|
||||
.filter(
|
||||
AgentVectorMemory.scope_kind == SCOPE_AGENT,
|
||||
or_(
|
||||
AgentVectorMemory.scope_id == agent_id,
|
||||
AgentVectorMemory.scope_id.like(f"%:{agent_id}"),
|
||||
AgentVectorMemory.scope_id.like(f"{current_user.id}:%"),
|
||||
),
|
||||
)
|
||||
.all()
|
||||
)
|
||||
return MemoryExportResponse(
|
||||
agent_id=agent_id,
|
||||
exported_at=datetime.utcnow().isoformat(),
|
||||
global_knowledge=[_serialize(gk) for gk in gk_list],
|
||||
knowledge_entities=[_serialize(ke) for ke in ke_list],
|
||||
knowledge_relations=[_serialize(kr) for kr in kr_list],
|
||||
learning_patterns=[_serialize(lp) for lp in lp_list],
|
||||
vector_memories=[_serialize(vm) for vm in vm_list],
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{agent_id}/memory/import")
|
||||
def import_memory(
|
||||
agent_id: str,
|
||||
body: MemoryImportRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
_check_agent(db, agent_id, current_user)
|
||||
imported = {"global_knowledge": 0, "knowledge_entities": 0, "knowledge_relations": 0, "learning_patterns": 0}
|
||||
|
||||
for item in body.global_knowledge:
|
||||
gk = GlobalKnowledge(
|
||||
id=str(uuid.uuid4()),
|
||||
content=item.get("content", ""),
|
||||
tags=item.get("tags"),
|
||||
confidence=item.get("confidence", "medium"),
|
||||
scope_kind=SCOPE_AGENT,
|
||||
scope_id=agent_id,
|
||||
source_agent_id=item.get("source_agent_id"),
|
||||
source_user_id=current_user.id,
|
||||
)
|
||||
db.add(gk)
|
||||
imported["global_knowledge"] += 1
|
||||
|
||||
for item in body.knowledge_entities:
|
||||
ke = KnowledgeEntity(
|
||||
id=str(uuid.uuid4()),
|
||||
name=item.get("name", ""),
|
||||
entity_type=item.get("entity_type", "concept"),
|
||||
description=item.get("description"),
|
||||
source="imported",
|
||||
confidence=item.get("confidence", "medium"),
|
||||
metadata_=item.get("metadata") or item.get("tags"),
|
||||
scope_kind=SCOPE_AGENT,
|
||||
scope_id=agent_id,
|
||||
user_id=current_user.id,
|
||||
)
|
||||
db.add(ke)
|
||||
imported["knowledge_entities"] += 1
|
||||
|
||||
for item in body.knowledge_relations:
|
||||
kr = KnowledgeRelation(
|
||||
id=str(uuid.uuid4()),
|
||||
source_entity_id=item.get("source_entity_id", ""),
|
||||
target_entity_id=item.get("target_entity_id", ""),
|
||||
relation_type=item.get("relation_type", "related_to"),
|
||||
description=item.get("description"),
|
||||
weight=item.get("weight", "1.0"),
|
||||
scope_kind=SCOPE_AGENT,
|
||||
scope_id=agent_id,
|
||||
)
|
||||
db.add(kr)
|
||||
imported["knowledge_relations"] += 1
|
||||
|
||||
for item in body.learning_patterns:
|
||||
lp = AgentLearningPattern(
|
||||
id=str(uuid.uuid4()),
|
||||
scope_kind=SCOPE_AGENT,
|
||||
scope_id=agent_id,
|
||||
task_category=item.get("task_category", "general"),
|
||||
task_keywords=item.get("task_keywords", ""),
|
||||
suggested_tools=item.get("suggested_tools", "[]"),
|
||||
effectiveness_score=item.get("effectiveness_score", 0.0),
|
||||
total_runs=item.get("total_runs", 1),
|
||||
successful_runs=item.get("successful_runs", 1),
|
||||
avg_iterations=item.get("avg_iterations", 1.0),
|
||||
avg_tool_calls=item.get("avg_tool_calls", 1.0),
|
||||
)
|
||||
db.add(lp)
|
||||
imported["learning_patterns"] += 1
|
||||
|
||||
imported["vector_memories"] = 0
|
||||
for item in body.vector_memories:
|
||||
vm = AgentVectorMemory(
|
||||
id=str(uuid.uuid4()),
|
||||
scope_kind=SCOPE_AGENT,
|
||||
scope_id=agent_id,
|
||||
session_key=item.get("session_key", ""),
|
||||
content_text=item.get("content_text", ""),
|
||||
metadata_=item.get("metadata"),
|
||||
)
|
||||
db.add(vm)
|
||||
imported["vector_memories"] += 1
|
||||
|
||||
db.commit()
|
||||
return {"ok": True, "imported": imported}
|
||||
@@ -151,6 +151,7 @@ class Settings(BaseSettings):
|
||||
class Config:
|
||||
env_file = str(_ENV_PATH)
|
||||
case_sensitive = True
|
||||
extra = "ignore"
|
||||
|
||||
|
||||
settings = Settings()
|
||||
|
||||
@@ -510,7 +510,7 @@ async def startup_event():
|
||||
logger.error(f"定时任务调度器启动失败: {e}")
|
||||
|
||||
# 注册路由
|
||||
from app.api import auth, workspaces, uploads, workflows, executions, websocket, execution_logs, data_sources, agents, platform_templates, model_configs, webhooks, template_market, batch_operations, collaboration, permissions, monitoring, alert_rules, node_test, node_templates, tools, agent_chat, agent_branches, agent_monitoring, knowledge_base, knowledge_dashboard, agent_schedules, notifications, feishu_bind, approval, orchestration_templates, plugins, agent_market, goals, tasks, system_logs, audit_logs, feedback, agent_swarm, push, voice, fcm, scene_contracts, teams
|
||||
from app.api import auth, workspaces, uploads, workflows, executions, websocket, execution_logs, data_sources, agents, platform_templates, model_configs, webhooks, template_market, batch_operations, collaboration, permissions, monitoring, alert_rules, node_test, node_templates, tools, agent_chat, agent_branches, agent_monitoring, knowledge_base, knowledge_dashboard, agent_schedules, notifications, feishu_bind, approval, orchestration_templates, plugins, agent_market, goals, tasks, system_logs, audit_logs, feedback, agent_swarm, push, voice, fcm, scene_contracts, teams, agent_memory
|
||||
|
||||
app.include_router(auth.router)
|
||||
app.include_router(workspaces.router)
|
||||
@@ -556,6 +556,7 @@ app.include_router(fcm.router)
|
||||
app.include_router(knowledge_dashboard.router)
|
||||
app.include_router(scene_contracts.router)
|
||||
app.include_router(teams.router)
|
||||
app.include_router(agent_memory.router)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
@@ -11,7 +11,7 @@ class AgentLearningPattern(Base):
|
||||
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
scope_kind = Column(String(16), nullable=False, index=True, comment="作用域类型: agent/bare")
|
||||
scope_id = Column(String(64), nullable=False, index=True, comment="作用域 ID: agent_id/user_id")
|
||||
scope_id = Column(String(255), nullable=False, index=True, comment="作用域 ID: agent_id/user_id")
|
||||
task_category = Column(String(64), nullable=False, default="general", comment="任务分类")
|
||||
task_keywords = Column(String(256), default="", comment="任务关键词")
|
||||
suggested_tools = Column(Text, nullable=False, comment="推荐工具序列 (JSON array)")
|
||||
|
||||
@@ -21,7 +21,7 @@ class AgentVectorMemory(Base):
|
||||
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
scope_kind = Column(String(16), nullable=False, index=True, comment="作用域类型: agent / bare")
|
||||
scope_id = Column(String(36), nullable=False, index=True, comment="作用域 ID: agent_id / user_id")
|
||||
scope_id = Column(String(255), nullable=False, index=True, comment="作用域 ID: agent_id / user_id")
|
||||
session_key = Column(String(128), nullable=False, default="", comment="会话标识")
|
||||
content_text = Column(Text, nullable=False, comment="原始对话文本")
|
||||
embedding = Column(Text, nullable=True, comment="JSON 序列化的 embedding 向量")
|
||||
|
||||
@@ -19,7 +19,7 @@ class PersistentUserMemory(Base):
|
||||
|
||||
id = Column(CHAR(36), primary_key=True, default=lambda: str(uuid.uuid4()), comment="主键")
|
||||
scope_kind = Column(String(16), nullable=False, comment="agent 或 workflow")
|
||||
scope_id = Column(CHAR(36), nullable=False, comment="Agent ID 或 Workflow ID")
|
||||
scope_id = Column(String(128), nullable=False, comment="Agent ID 或 Workflow ID")
|
||||
session_key = Column(String(512), nullable=False, comment="调用方传入的 user_id 等会话键")
|
||||
payload = Column(JSON, nullable=False, comment="与 Redis 中 user_memory_* 结构一致的记忆 JSON")
|
||||
updated_at = Column(
|
||||
|
||||
Reference in New Issue
Block a user