添加知识库与子代理功能。添加 debug 模式。
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -36,30 +36,18 @@ public class OmniRealtimeClient {
|
||||
private static final String INPUT_AUDIO_TRANSCRIPTION_MODEL = "gummy-realtime-v1";
|
||||
private static final int MAX_IMAGE_BYTES = 500 * 1024;
|
||||
private static final long MIN_IMAGE_APPEND_INTERVAL_MS = 800L;
|
||||
private static final String DEFAULT_INSTRUCTIONS = "你是一名面向中国老年群体的心理陪伴 AI。你的职责是为老人提供温和、耐心、尊重、稳定的情感陪伴、情绪支持、回忆唤起、生活关怀和风险识别服务。你不是医生,也不是心理治疗师,不能替代专业诊疗,但应在必要时建议联系家属、社区、医生或急救资源。\n"
|
||||
private static final String DEFAULT_INSTRUCTIONS = "你是一名面向中国老年群体的心理陪伴 AI。你的职责是为老人提供温和、耐心、尊重、稳定的情感陪伴、情绪支持、回忆唤起、生活关怀和风险识别服务。\n"
|
||||
+ "\n"
|
||||
+ "请始终遵循以下原则:\n"
|
||||
+ "\n"
|
||||
+ "先共情,再回应问题;\n"
|
||||
+ "\n"
|
||||
+ "先接住情绪,再给建议;\n"
|
||||
+ "\n"
|
||||
+ "使用简短、朴素、生活化的中文,始终用“您”称呼;\n"
|
||||
+ "\n"
|
||||
+ "不说教、不训诫、不否定、不敷衍;\n"
|
||||
+ "\n"
|
||||
+ "不把老人当小孩,不制造虚假亲密;\n"
|
||||
+ "\n"
|
||||
+ "一次不提供超过两个建议;\n"
|
||||
+ "\n"
|
||||
+ "鼓励老人表达感受,可适度引导回忆人生经历、兴趣、家庭和过往成就;\n"
|
||||
+ "\n"
|
||||
+ "对孤独、丧偶、被忽视、无价值感、失眠、焦虑、怕拖累家人等议题保持高度敏感;\n"
|
||||
+ "\n"
|
||||
+ "若出现自杀、自伤、重度绝望、幻觉妄想、意识混乱、虐待、严重医疗异常等风险,必须立即建议联系真人帮助和专业机构;\n"
|
||||
+ "\n"
|
||||
+ "不编造医疗知识、政策信息、机构电话,不确定时明确说明。\n"
|
||||
+ "\n"
|
||||
+ "你的回复风格应温和、平稳、尊重、有耐心。每次回答尽量包含:一句共情、一句理解或复述、一句温和追问或一个微小建议。核心目标是让老人感到:我被听见了,我被尊重了,有人愿意陪我。";
|
||||
|
||||
private final OkHttpClient okHttpClient;
|
||||
|
||||
@@ -4,6 +4,9 @@ public final class DashScopeConfig {
|
||||
|
||||
public static final String DEFAULT_API_KEY = "sk-7c32083ace99472e97209e611b9ecbb1";
|
||||
public static final String REALTIME_BASE_URL = "wss://dashscope.aliyuncs.com/api-ws/v1/realtime";
|
||||
public static final String MULTIMODAL_GENERATION_URL = "https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation";
|
||||
public static final String PSYCHOLOGY_SUBAGENT_MODEL = "qwen3.6-plus";
|
||||
public static final String PSYCHOLOGY_KNOWLEDGE_ASSET_PATH = "knowledge/who_icope_2024.txt";
|
||||
public static final String VOICE_CLONE_URL = "https://dashscope.aliyuncs.com/api/v1/services/audio/tts/customization";
|
||||
public static final String VOICE_CLONE_MODEL = "qwen-voice-enrollment";
|
||||
public static final String VOICE_CLONE_TTS_MODEL = "qwen3-tts-vc-realtime-2026-01-15";
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package ai.guiji.duix.test.service
|
||||
|
||||
import android.content.Context
|
||||
|
||||
class DebugSettingsStore(context: Context) {
|
||||
|
||||
private val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
fun isChatDebugEnabled(): Boolean {
|
||||
return prefs.getBoolean(KEY_CHAT_DEBUG_ENABLED, false)
|
||||
}
|
||||
|
||||
fun setChatDebugEnabled(enabled: Boolean) {
|
||||
prefs.edit().putBoolean(KEY_CHAT_DEBUG_ENABLED, enabled).apply()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREFS_NAME = "app_debug_settings"
|
||||
private const val KEY_CHAT_DEBUG_ENABLED = "chat_debug_enabled"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
package ai.guiji.duix.test.service
|
||||
|
||||
import ai.guiji.duix.test.App
|
||||
import android.content.Context
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class PsychologyKnowledgeAgentService(
|
||||
context: Context,
|
||||
) {
|
||||
|
||||
private val appContext = context.applicationContext
|
||||
private val client = App.getOkHttpClient().newBuilder()
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.writeTimeout(120, TimeUnit.SECONDS)
|
||||
.readTimeout(180, TimeUnit.SECONDS)
|
||||
.callTimeout(180, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
private val knowledgeText: String by lazy {
|
||||
appContext.assets.open(DashScopeConfig.PSYCHOLOGY_KNOWLEDGE_ASSET_PATH)
|
||||
.bufferedReader(Charsets.UTF_8)
|
||||
.use { reader ->
|
||||
reader.readText()
|
||||
.replace('\u000c', '\n')
|
||||
.replace("\u0000", "")
|
||||
}
|
||||
}
|
||||
|
||||
fun consultKnowledge(
|
||||
question: String,
|
||||
focus: String? = null,
|
||||
): KnowledgeAnswer {
|
||||
val normalizedQuestion = question.trim()
|
||||
require(normalizedQuestion.isNotEmpty()) { "问题不能为空" }
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(DashScopeConfig.MULTIMODAL_GENERATION_URL)
|
||||
.addHeader("Authorization", "Bearer " + DashScopeConfig.DEFAULT_API_KEY)
|
||||
.addHeader("Content-Type", "application/json")
|
||||
.post(
|
||||
buildRequestPayload(
|
||||
normalizedQuestion,
|
||||
focus?.trim().takeIf { !it.isNullOrEmpty() },
|
||||
).toString().toRequestBody(JSON_MEDIA_TYPE)
|
||||
)
|
||||
.build()
|
||||
|
||||
client.newCall(request).execute().use { response ->
|
||||
val body = response.body?.string().orEmpty()
|
||||
if (!response.isSuccessful) {
|
||||
throw IllegalStateException(parseErrorMessage(body).ifEmpty { "心理学知识子代理调用失败" })
|
||||
}
|
||||
|
||||
val json = JSONObject(body)
|
||||
val message = json.optJSONObject("output")
|
||||
?.optJSONArray("choices")
|
||||
?.optJSONObject(0)
|
||||
?.optJSONObject("message")
|
||||
val content = extractMessageContent(message)
|
||||
if (content.isBlank()) {
|
||||
throw IllegalStateException("心理学知识子代理没有返回内容")
|
||||
}
|
||||
|
||||
val usage = json.optJSONObject("usage")
|
||||
return KnowledgeAnswer(
|
||||
outputText = content.trim(),
|
||||
model = DashScopeConfig.PSYCHOLOGY_SUBAGENT_MODEL,
|
||||
requestId = json.optString("request_id").ifBlank { null },
|
||||
inputTokens = usage?.optInt("input_tokens") ?: 0,
|
||||
outputTokens = usage?.optInt("output_tokens") ?: 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildRequestPayload(
|
||||
question: String,
|
||||
focus: String?,
|
||||
): JSONObject {
|
||||
return JSONObject().apply {
|
||||
put("model", DashScopeConfig.PSYCHOLOGY_SUBAGENT_MODEL)
|
||||
put(
|
||||
"input",
|
||||
JSONObject().apply {
|
||||
put(
|
||||
"messages",
|
||||
JSONArray()
|
||||
.put(
|
||||
JSONObject().apply {
|
||||
put("role", "system")
|
||||
put("content", buildSystemPrompt())
|
||||
}
|
||||
)
|
||||
.put(
|
||||
JSONObject().apply {
|
||||
put("role", "user")
|
||||
put("content", buildUserPrompt(question, focus))
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
put(
|
||||
"parameters",
|
||||
JSONObject().apply {
|
||||
put("result_format", "message")
|
||||
put("enable_thinking", false)
|
||||
put("temperature", 0.2)
|
||||
put("max_tokens", 900)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildSystemPrompt(): String {
|
||||
return buildString {
|
||||
append(knowledgeText)
|
||||
append("以上是完整知识库全文\n")
|
||||
append("你是一个面向老年陪伴场景的权威照护知识子代理。")
|
||||
append("你只能依据系统里提供的 WHO ICOPE 2024 全文回答,不要编造,不要输出与原文冲突的结论。")
|
||||
append("如果知识库未提供足够直接依据,要明确说明。")
|
||||
append("请使用简体中文输出,提供权威心理学指示和说明。不需要包括生活化语言,你只是一个查询专家,不负责指导。直接输出无格式纯文本,答案简练不超过100字。")
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildUserPrompt(
|
||||
question: String,
|
||||
focus: String?,
|
||||
): String {
|
||||
return buildString {
|
||||
append("请基于上面的 WHO ICOPE 2024 全文,回答下面这个需要查询的问题。\n")
|
||||
append("核心问题:")
|
||||
append(question)
|
||||
if (!focus.isNullOrBlank()) {
|
||||
append("\n特别关注:")
|
||||
append(focus)
|
||||
}
|
||||
append("\n请避免空泛安慰,优先给出可落地的权威说明。")
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractMessageContent(message: JSONObject?): String {
|
||||
if (message == null) {
|
||||
return ""
|
||||
}
|
||||
val content = message.opt("content")
|
||||
return when (content) {
|
||||
is String -> content
|
||||
is JSONArray -> buildString {
|
||||
for (index in 0 until content.length()) {
|
||||
val part = content.opt(index)
|
||||
val text = when (part) {
|
||||
is JSONObject -> part.optString("text")
|
||||
is String -> part
|
||||
else -> ""
|
||||
}.trim()
|
||||
if (text.isNotEmpty()) {
|
||||
if (isNotEmpty()) {
|
||||
append('\n')
|
||||
}
|
||||
append(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseErrorMessage(body: String): String {
|
||||
return try {
|
||||
val json = JSONObject(body)
|
||||
json.optJSONObject("error")?.optString("message").orEmpty()
|
||||
.ifBlank { json.optString("message") }
|
||||
} catch (_: Exception) {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
data class KnowledgeAnswer(
|
||||
val outputText: String,
|
||||
val model: String,
|
||||
val requestId: String?,
|
||||
val inputTokens: Int,
|
||||
val outputTokens: Int,
|
||||
)
|
||||
|
||||
companion object {
|
||||
private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaType()
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,9 @@ import ai.guiji.duix.test.databinding.ActivityCallBinding
|
||||
import ai.guiji.duix.test.realtime.OmniRealtimeClient
|
||||
import ai.guiji.duix.test.realtime.QwenTtsRealtimeClient
|
||||
import ai.guiji.duix.test.service.DashScopeConfig
|
||||
import ai.guiji.duix.test.service.DebugSettingsStore
|
||||
import ai.guiji.duix.test.service.FamilyBridgeService
|
||||
import ai.guiji.duix.test.service.PsychologyKnowledgeAgentService
|
||||
import ai.guiji.duix.test.ui.adapter.MotionAdapter
|
||||
import ai.guiji.duix.test.ui.dialog.AudioRecordDialog
|
||||
import ai.guiji.duix.test.ui.widget.AudioSpectrumView
|
||||
@@ -32,9 +34,7 @@ import android.widget.Toast
|
||||
import com.bumptech.glide.Glide
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
@@ -55,18 +55,22 @@ class CallActivity : BaseActivity() {
|
||||
private fun applyMessage(msg: String){
|
||||
if (debug){
|
||||
runOnUiThread {
|
||||
binding.tvDebug.visibility = View.VISIBLE
|
||||
if (mMessage.length > 10000){
|
||||
mMessage = ""
|
||||
binding.scrollDebug.visibility = View.VISIBLE
|
||||
if (mMessage.length > 60000){
|
||||
mMessage = mMessage.takeLast(40000)
|
||||
}
|
||||
mMessage = "${StringUtils.dateToStringMS4()} $msg\n$mMessage"
|
||||
mMessage += "${StringUtils.dateToStringMS4()} $msg\n"
|
||||
binding.tvDebug.text = mMessage
|
||||
binding.scrollDebug.post {
|
||||
binding.scrollDebug.fullScroll(View.FOCUS_DOWN)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private lateinit var binding: ActivityCallBinding
|
||||
private val debugSettingsStore by lazy { DebugSettingsStore(applicationContext) }
|
||||
private var duix: DUIX? = null
|
||||
private var mDUIXRender: DUIXRenderer? = null
|
||||
private var mModelInfo: ModelInfo?=null // 加载的模型信息
|
||||
@@ -80,13 +84,14 @@ class CallActivity : BaseActivity() {
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
private var assistantTextBuffer = StringBuilder()
|
||||
private var hasStartedPushForResponse = false
|
||||
private val enableWebSearch = false
|
||||
private val enableWebSearch = true
|
||||
private val enableToolCalling = true
|
||||
private var isCloneMode = false
|
||||
private var clonedVoice = ""
|
||||
private var cloneTtsModel = DashScopeConfig.VOICE_CLONE_TTS_MODEL
|
||||
private var ttsRealtimeClient: QwenTtsRealtimeClient? = null
|
||||
private val familyBridgeService by lazy { FamilyBridgeService(applicationContext) }
|
||||
private val psychologyKnowledgeAgentService by lazy { PsychologyKnowledgeAgentService(applicationContext) }
|
||||
private val bridgeExecutor: ExecutorService = Executors.newSingleThreadExecutor()
|
||||
private var currentSessionId: String? = null
|
||||
private var pendingConversationPrompt: FamilyBridgeService.ConversationPromptInjection? = null
|
||||
@@ -103,7 +108,7 @@ class CallActivity : BaseActivity() {
|
||||
|
||||
modelUrl = intent.getStringExtra("modelUrl") ?: ""
|
||||
voice = intent.getStringExtra("voice") ?: "Ethan"
|
||||
debug = intent.getBooleanExtra("debug", false)
|
||||
debug = intent.getBooleanExtra("debug", debugSettingsStore.isChatDebugEnabled())
|
||||
isCloneMode = intent.getBooleanExtra("cloneMode", false)
|
||||
clonedVoice = intent.getStringExtra("clonedVoice") ?: ""
|
||||
cloneTtsModel = intent.getStringExtra("cloneTtsModel") ?: DashScopeConfig.VOICE_CLONE_TTS_MODEL
|
||||
@@ -136,16 +141,6 @@ class CallActivity : BaseActivity() {
|
||||
requestPermission(arrayOf(Manifest.permission.RECORD_AUDIO), 1)
|
||||
}
|
||||
|
||||
binding.btnPlayPCM.setOnClickListener {
|
||||
applyMessage("start play pcm")
|
||||
playPCMStream()
|
||||
}
|
||||
|
||||
binding.btnPlayWAV.setOnClickListener {
|
||||
applyMessage("start play wav")
|
||||
playWAVFile()
|
||||
}
|
||||
|
||||
binding.btnRandomMotion.setOnClickListener {
|
||||
applyMessage("start random motion")
|
||||
duix?.startRandomMotion(true)
|
||||
@@ -238,16 +233,9 @@ class CallActivity : BaseActivity() {
|
||||
applyMessage("init ok")
|
||||
runOnUiThread {
|
||||
binding.btnRecord.isEnabled = true
|
||||
binding.btnPlayPCM.isEnabled = true
|
||||
binding.btnPlayWAV.isEnabled = true
|
||||
binding.switchMute.isEnabled = true
|
||||
binding.btnAIConversation.isEnabled = true
|
||||
|
||||
if (debug) {
|
||||
binding.btnPlayPCM.visibility = View.VISIBLE
|
||||
binding.btnPlayWAV.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
mModelInfo?.let { modelInfo ->
|
||||
if (modelInfo.motionRegions.isNotEmpty()) {
|
||||
val names = ArrayList<String>()
|
||||
@@ -305,6 +293,9 @@ class CallActivity : BaseActivity() {
|
||||
return buildString {
|
||||
append(OmniRealtimeClient.getDefaultInstructions())
|
||||
append("\n\n")
|
||||
append("当用户需要关于记忆下降、情绪低落、焦虑失眠、孤独感、认知照护、老年照护路径或家属陪伴方式的权威依据时,优先调用 consult_psychology_knowledge_base 工具,不要凭经验给出确定性的专业判断。\n")
|
||||
append("调用 consult_psychology_knowledge_base 时,把老人当前最核心的问题写入 question;如果你想限定查询角度,例如陪伴建议、风险判断、是否需要就医,就写入 focus。\n")
|
||||
append("如果只是日常闲聊或普通安慰,不必调用 consult_psychology_knowledge_base 工具。\n")
|
||||
append("当用户明确要给儿子、女儿、老伴、家里人或亲友带话、留言、报平安时,必须调用 leave_message_for_family 工具,不要假装已经转达。\n")
|
||||
append("当用户问家里人有没有留言、想听家人说了什么、想看看新消息时,必须调用 fetch_family_messages 工具,不要自己编造留言。\n")
|
||||
append("工具执行结果返回后,再用温和、简短、生活化的中文向老人说明情况。")
|
||||
@@ -350,7 +341,27 @@ class CallActivity : BaseActivity() {
|
||||
})
|
||||
}
|
||||
|
||||
val psychologyKnowledgeParameters = JSONObject().apply {
|
||||
put("type", "object")
|
||||
put("properties", JSONObject().apply {
|
||||
put("question", JSONObject().apply {
|
||||
put("type", "string")
|
||||
put("description", "需要从权威知识库中查询的核心问题,应尽量包含老人当前的真实困扰或具体疑问。")
|
||||
})
|
||||
put("focus", JSONObject().apply {
|
||||
put("type", "string")
|
||||
put("description", "希望子代理特别聚焦的角度,例如陪伴建议、风险判断、何时就医、家属沟通方式。")
|
||||
})
|
||||
})
|
||||
put("required", JSONArray().put("question"))
|
||||
}
|
||||
|
||||
return listOf(
|
||||
OmniRealtimeClient.ToolDefinition(
|
||||
"consult_psychology_knowledge_base",
|
||||
"当需要基于 WHO ICOPE 2024 等权威照护知识回答认知、情绪、睡眠、孤独、照护与家属支持问题时使用。当老人提出具体问题比如食欲不振时,必须使用此工具进行查询,而不是凭经验直接回答。",
|
||||
psychologyKnowledgeParameters,
|
||||
),
|
||||
OmniRealtimeClient.ToolDefinition(
|
||||
"leave_message_for_family",
|
||||
"当老人明确要求给家里人带话、留言、报平安或请转告时使用。",
|
||||
@@ -385,46 +396,6 @@ class CallActivity : BaseActivity() {
|
||||
bridgeExecutor.shutdownNow()
|
||||
}
|
||||
|
||||
private fun playPCMStream(){
|
||||
val thread = Thread {
|
||||
duix?.startPush()
|
||||
val inputStream = assets.open("pcm/2.pcm")
|
||||
val buffer = ByteArray(320)
|
||||
var length: Int
|
||||
while (inputStream.read(buffer).also { length = it } > 0){
|
||||
val data = buffer.copyOfRange(0, length)
|
||||
duix?.pushPcm(data)
|
||||
}
|
||||
duix?.stopPush()
|
||||
inputStream.close()
|
||||
}
|
||||
thread.start()
|
||||
}
|
||||
|
||||
private fun playWAVFile(){
|
||||
val thread = Thread {
|
||||
val wavName = "1.wav"
|
||||
val wavFile = File(mContext.externalCacheDir, wavName)
|
||||
if (!wavFile.exists()){
|
||||
// copy assets -> sd card
|
||||
val inputStream = assets.open("wav/$wavName")
|
||||
if (!mContext.externalCacheDir!!.exists()){
|
||||
mContext.externalCacheDir!!.mkdirs()
|
||||
}
|
||||
val out = FileOutputStream(wavFile)
|
||||
val buffer = ByteArray(1024)
|
||||
var length: Int
|
||||
while ((inputStream.read(buffer).also { length = it }) > 0) {
|
||||
out.write(buffer, 0, length)
|
||||
}
|
||||
out.close()
|
||||
inputStream.close()
|
||||
}
|
||||
duix?.playAudio(wavFile.absolutePath)
|
||||
}
|
||||
thread.start()
|
||||
}
|
||||
|
||||
override fun permissionsGet(get: Boolean, code: Int) {
|
||||
super.permissionsGet(get, code)
|
||||
when (code) {
|
||||
@@ -496,6 +467,9 @@ class CallActivity : BaseActivity() {
|
||||
binding.btnCameraToggle.contentDescription = getString(R.string.camera_off)
|
||||
isCameraEnabled = false
|
||||
applyMessage("AI对话: 已连接, model=${OmniRealtimeClient.DEFAULT_MODEL}, search=${if (enableWebSearch) "on" else "off"}, tools=${if (enableToolCalling) "on" else "off"}, cloneMode=$isCloneMode")
|
||||
if (enableWebSearch && enableToolCalling) {
|
||||
// applyMessage("AI对话: 当前配置请求 search=on 且 tools=on,但 Qwen Omni Realtime 协议规定两者不兼容,实际下发时将由服务端连接配置决定仅生效其中一项")
|
||||
}
|
||||
if (initialConversationPrompt != null) {
|
||||
applyMessage("AI对话: 检测到家属留言,准备主动播报")
|
||||
val requested = omniClient?.requestResponse() == true
|
||||
@@ -554,6 +528,7 @@ class CallActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
override fun onUserTranscript(transcript: String) {
|
||||
applyMessage("用户语音转写: $transcript")
|
||||
mainHandler.post {
|
||||
binding.tvSubtitle.text = getString(R.string.ai_call_user_said, transcript)
|
||||
}
|
||||
@@ -568,6 +543,7 @@ class CallActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
override fun onAssistantTranscriptDone(transcript: String) {
|
||||
applyMessage("助手回复: $transcript")
|
||||
mainHandler.post {
|
||||
assistantTextBuffer.clear()
|
||||
assistantTextBuffer.append(transcript)
|
||||
@@ -579,6 +555,7 @@ class CallActivity : BaseActivity() {
|
||||
override fun onToolCallRequested(toolName: String, callId: String, argumentsJson: String) {
|
||||
mainHandler.post {
|
||||
binding.tvSubtitle.text = when (toolName) {
|
||||
"consult_psychology_knowledge_base" -> "我来帮您查一下更稳妥的照护资料。"
|
||||
"leave_message_for_family" -> "我来帮您把这句话记给家里人。"
|
||||
"fetch_family_messages" -> "我来帮您看看家里人的新留言。"
|
||||
else -> getString(R.string.ai_call_thinking)
|
||||
@@ -888,6 +865,7 @@ class CallActivity : BaseActivity() {
|
||||
enqueueBridgeTask("工具调用处理失败") {
|
||||
val arguments = runCatching { JSONObject(argumentsJson) }.getOrElse { JSONObject() }
|
||||
val result = executeRealtimeTool(toolName, callId, arguments)
|
||||
applyMessage("AI工具输出: $toolName, callId=$callId, status=${result.status}, output=${result.outputText}")
|
||||
uploadConversationTurnInternal(
|
||||
FamilyBridgeService.ROLE_TOOL,
|
||||
result.outputText,
|
||||
@@ -911,6 +889,7 @@ class CallActivity : BaseActivity() {
|
||||
arguments: JSONObject,
|
||||
): ToolExecutionResult {
|
||||
return when (toolName) {
|
||||
"consult_psychology_knowledge_base" -> executePsychologyKnowledgeTool(callId, arguments)
|
||||
"leave_message_for_family" -> executeLeaveMessageTool(callId, arguments)
|
||||
"fetch_family_messages" -> executeFetchFamilyMessagesTool(callId, arguments)
|
||||
else -> {
|
||||
@@ -930,6 +909,56 @@ class CallActivity : BaseActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun executePsychologyKnowledgeTool(callId: String, arguments: JSONObject): ToolExecutionResult {
|
||||
val question = arguments.optString("question").trim()
|
||||
val focus = arguments.optString("focus").trim().ifBlank { null }
|
||||
|
||||
if (question.isEmpty()) {
|
||||
val outputText = "还没有拿到要查询的具体问题,请先用一句话说明想查什么。"
|
||||
runCatching {
|
||||
familyBridgeService.recordToolCall(
|
||||
toolName = "consult_psychology_knowledge_base",
|
||||
arguments = arguments,
|
||||
outputText = outputText,
|
||||
status = FamilyBridgeService.STATUS_FAILURE,
|
||||
callId = callId,
|
||||
sessionId = currentSessionId,
|
||||
)
|
||||
}
|
||||
return ToolExecutionResult(outputText, FamilyBridgeService.STATUS_FAILURE)
|
||||
}
|
||||
|
||||
return runCatching {
|
||||
val result = psychologyKnowledgeAgentService.consultKnowledge(question, focus)
|
||||
familyBridgeService.recordToolCall(
|
||||
toolName = "consult_psychology_knowledge_base",
|
||||
arguments = arguments,
|
||||
outputText = result.outputText,
|
||||
status = FamilyBridgeService.STATUS_SUCCESS,
|
||||
callId = callId,
|
||||
sessionId = currentSessionId,
|
||||
)
|
||||
applyMessage(
|
||||
"心理学子代理完成: model=${result.model}, requestId=${result.requestId ?: "-"}, inputTokens=${result.inputTokens}, outputTokens=${result.outputTokens}"
|
||||
)
|
||||
ToolExecutionResult(result.outputText, FamilyBridgeService.STATUS_SUCCESS)
|
||||
}.getOrElse { error ->
|
||||
val outputText = "权威照护资料查询暂时失败。请先基于安全原则回应:保持温和陪伴,避免给出确定性的医学结论;如果涉及持续恶化、明显痛苦、自伤他伤或急性危险,请建议尽快联系家属、医生或急救。"
|
||||
runCatching {
|
||||
familyBridgeService.recordToolCall(
|
||||
toolName = "consult_psychology_knowledge_base",
|
||||
arguments = arguments,
|
||||
outputText = outputText,
|
||||
status = FamilyBridgeService.STATUS_FAILURE,
|
||||
callId = callId,
|
||||
sessionId = currentSessionId,
|
||||
)
|
||||
}
|
||||
applyMessage("心理学子代理失败: ${error.message}")
|
||||
ToolExecutionResult(outputText, FamilyBridgeService.STATUS_FAILURE)
|
||||
}
|
||||
}
|
||||
|
||||
private fun executeLeaveMessageTool(callId: String, arguments: JSONObject): ToolExecutionResult {
|
||||
val message = arguments.optString("message").trim()
|
||||
val relation = arguments.optString("recipientRelation").trim().ifBlank { "家里人" }
|
||||
|
||||
@@ -5,6 +5,7 @@ import ai.guiji.duix.test.R
|
||||
import ai.guiji.duix.test.databinding.ActivityMainBinding
|
||||
import ai.guiji.duix.test.model.AvatarCatalog
|
||||
import ai.guiji.duix.test.model.AvatarModel
|
||||
import ai.guiji.duix.test.service.DebugSettingsStore
|
||||
import ai.guiji.duix.test.service.FamilyBridgeService
|
||||
import ai.guiji.duix.test.ui.adapter.AvatarAdapter
|
||||
import ai.guiji.duix.test.ui.dialog.LoadingDialog
|
||||
@@ -19,6 +20,7 @@ import java.io.File
|
||||
class MainActivity : BaseActivity() {
|
||||
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private val debugSettingsStore by lazy { DebugSettingsStore(applicationContext) }
|
||||
private val familyBridgeService by lazy { FamilyBridgeService(applicationContext) }
|
||||
private var mLoadingDialog: LoadingDialog? = null
|
||||
private var mLastProgress = 0
|
||||
@@ -89,7 +91,7 @@ class MainActivity : BaseActivity() {
|
||||
val intent = Intent(mContext, CallActivity::class.java)
|
||||
intent.putExtra("modelUrl", model.modelUrl)
|
||||
intent.putExtra("voice", model.voice)
|
||||
intent.putExtra("debug", false)
|
||||
intent.putExtra("debug", debugSettingsStore.isChatDebugEnabled())
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package ai.guiji.duix.test.ui.activity
|
||||
import ai.guiji.duix.test.BuildConfig
|
||||
import ai.guiji.duix.test.databinding.ActivitySettingsBinding
|
||||
import ai.guiji.duix.test.service.DashScopeConfig
|
||||
import ai.guiji.duix.test.service.DebugSettingsStore
|
||||
import ai.guiji.duix.test.service.FamilyBridgeService
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
@@ -12,6 +13,7 @@ import org.json.JSONObject
|
||||
class SettingsActivity : BaseActivity() {
|
||||
|
||||
private lateinit var binding: ActivitySettingsBinding
|
||||
private val debugSettingsStore by lazy { DebugSettingsStore(applicationContext) }
|
||||
private val familyBridgeService by lazy { FamilyBridgeService(applicationContext) }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -23,11 +25,18 @@ class SettingsActivity : BaseActivity() {
|
||||
binding.rowDeviceCode.setOnClickListener {
|
||||
startActivity(Intent(mContext, DeviceSettingsActivity::class.java))
|
||||
}
|
||||
binding.rowDebugMode.setOnClickListener {
|
||||
binding.switchDebugMode.performClick()
|
||||
}
|
||||
|
||||
binding.tvVersionValue.text = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})"
|
||||
binding.tvServerValue.text = Uri.parse(DashScopeConfig.MONITOR_BASE_URL).host
|
||||
?: DashScopeConfig.MONITOR_BASE_URL
|
||||
binding.tvDeviceValue.text = familyBridgeService.getDeviceUuid()
|
||||
binding.switchDebugMode.isChecked = debugSettingsStore.isChatDebugEnabled()
|
||||
binding.switchDebugMode.setOnCheckedChangeListener { _, isChecked ->
|
||||
debugSettingsStore.setChatDebugEnabled(isChecked)
|
||||
}
|
||||
|
||||
Thread {
|
||||
runCatching {
|
||||
|
||||
@@ -7,6 +7,7 @@ import ai.guiji.duix.test.model.AvatarCatalog
|
||||
import ai.guiji.duix.test.model.AvatarModel
|
||||
import ai.guiji.duix.test.model.VoiceCloneProfile
|
||||
import ai.guiji.duix.test.service.DashScopeConfig
|
||||
import ai.guiji.duix.test.service.DebugSettingsStore
|
||||
import ai.guiji.duix.test.service.VoiceCloneProfileStore
|
||||
import ai.guiji.duix.test.service.VoiceCloneService
|
||||
import ai.guiji.duix.test.ui.adapter.AvatarAdapter
|
||||
@@ -27,6 +28,7 @@ class VoiceCloneSetupActivity : BaseActivity() {
|
||||
private lateinit var binding: ActivityVoiceCloneSetupBinding
|
||||
private lateinit var avatarAdapter: AvatarAdapter
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
private val debugSettingsStore by lazy { DebugSettingsStore(applicationContext) }
|
||||
private val profileStore by lazy { VoiceCloneProfileStore(this) }
|
||||
private val voiceCloneService = VoiceCloneService()
|
||||
private val recorder = VoiceCloneRecorder()
|
||||
@@ -222,7 +224,7 @@ class VoiceCloneSetupActivity : BaseActivity() {
|
||||
val intent = Intent(mContext, CallActivity::class.java)
|
||||
intent.putExtra("modelUrl", selectedAvatar.modelUrl)
|
||||
intent.putExtra("voice", selectedAvatar.voice)
|
||||
intent.putExtra("debug", false)
|
||||
intent.putExtra("debug", debugSettingsStore.isChatDebugEnabled())
|
||||
intent.putExtra("cloneMode", true)
|
||||
intent.putExtra("clonedVoice", profile.voice)
|
||||
intent.putExtra("cloneTtsModel", profile.targetModel)
|
||||
|
||||
@@ -15,16 +15,29 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvDebug"
|
||||
android:textSize="12sp"
|
||||
<ScrollView
|
||||
android:id="@+id/scrollDebug"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="220dp"
|
||||
android:visibility="gone"
|
||||
android:textColor="@color/white"
|
||||
android:background="#66000000"
|
||||
android:padding="6dp"
|
||||
android:fadeScrollbars="false"
|
||||
android:fillViewport="true"
|
||||
android:scrollbars="vertical"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="200dp"/>
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvDebug"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="monospace"
|
||||
android:padding="8dp"
|
||||
android:textColor="@color/white"
|
||||
android:textIsSelectable="true"
|
||||
android:textSize="12sp" />
|
||||
</ScrollView>
|
||||
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
@@ -160,30 +173,4 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnPlayPCM"
|
||||
android:text="@string/play_pcm"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toTopOf="@+id/btnAIConversation"
|
||||
android:background="@drawable/shape_common_btn"
|
||||
android:layout_margin="6dp"
|
||||
android:textSize="13sp"
|
||||
android:textColor="@color/white"
|
||||
android:enabled="false"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnPlayWAV"
|
||||
android:text="@string/play_wav"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toTopOf="@+id/btnPlayPCM"
|
||||
android:background="@drawable/shape_common_btn"
|
||||
android:layout_margin="6dp"
|
||||
android:textSize="13sp"
|
||||
android:textColor="@color/white"
|
||||
android:enabled="false"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -87,6 +87,53 @@
|
||||
android:contentDescription="@null" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/rowDebugMode"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@drawable/shape_warm_feature_card"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:padding="18dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/settings_debug_title"
|
||||
android:textColor="@color/warm_text_primary"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:lineSpacingExtra="4dp"
|
||||
android:text="@string/settings_debug_desc"
|
||||
android:textColor="@color/warm_text_secondary"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/switchDebugMode"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:contentDescription="@string/settings_debug_title"
|
||||
android:importantForAutofill="no"
|
||||
android:text="" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -80,6 +80,8 @@
|
||||
<string name="settings_subtitle">把设备信息、家人连接入口和当前版本都放在这里,找起来更方便。</string>
|
||||
<string name="settings_device_code_title">家人连接设备码</string>
|
||||
<string name="settings_device_code_desc">打开设备二维码页面后,家里人扫一扫就能把这台设备绑定到自己的账号。</string>
|
||||
<string name="settings_debug_title">显示调试详情</string>
|
||||
<string name="settings_debug_desc">开启后,聊天页面会显示完整的交互日志,包括工具调用的输入和输出。</string>
|
||||
<string name="settings_about_title">关于这台设备</string>
|
||||
<string name="settings_version_label">应用版本</string>
|
||||
<string name="settings_server_label">家属面板域名</string>
|
||||
|
||||
@@ -82,6 +82,8 @@
|
||||
<string name="settings_subtitle">Keep device information, family connection entry and current version together in one place.</string>
|
||||
<string name="settings_device_code_title">Family device code</string>
|
||||
<string name="settings_device_code_desc">Open the QR code page for your family to scan and bind this elder device.</string>
|
||||
<string name="settings_debug_title">Show debug details</string>
|
||||
<string name="settings_debug_desc">When enabled, the chat page will show full interaction logs, including tool input and output.</string>
|
||||
<string name="settings_about_title">About this device</string>
|
||||
<string name="settings_version_label">App version</string>
|
||||
<string name="settings_server_label">Family panel domain</string>
|
||||
|
||||
Reference in New Issue
Block a user