Add intent dispatch scaffold
This commit is contained in:
@@ -28,6 +28,11 @@ qt_add_executable(QtDesktopPet
|
||||
src/ai/GoogleGeminiProvider.cpp
|
||||
src/ai/OpenAICompatibleProvider.h
|
||||
src/ai/OpenAICompatibleProvider.cpp
|
||||
src/assistant/CommandDispatcher.h
|
||||
src/assistant/CommandDispatcher.cpp
|
||||
src/assistant/IntentRouter.h
|
||||
src/assistant/IntentRouter.cpp
|
||||
src/assistant/UserIntent.h
|
||||
src/character/AnimationClip.h
|
||||
src/character/AnimationClip.cpp
|
||||
src/character/CharacterPackage.h
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
||||
#include "CommandDispatcher.h"
|
||||
|
||||
QString userIntentTypeName(UserIntentType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case UserIntentType::Chat:
|
||||
return QStringLiteral("Chat");
|
||||
case UserIntentType::Reminder:
|
||||
return QStringLiteral("Reminder");
|
||||
case UserIntentType::Weather:
|
||||
return QStringLiteral("Weather");
|
||||
case UserIntentType::FileOperation:
|
||||
return QStringLiteral("FileOperation");
|
||||
case UserIntentType::Search:
|
||||
return QStringLiteral("Search");
|
||||
}
|
||||
|
||||
return QStringLiteral("Unknown");
|
||||
}
|
||||
|
||||
CommandDispatchResult CommandDispatcher::dispatch(const QString &text) const
|
||||
{
|
||||
const UserIntent intent = m_intentRouter.route(text);
|
||||
if (intent.type == UserIntentType::Chat)
|
||||
{
|
||||
return {CommandDispatchAction::Chat, intent, intent.text};
|
||||
}
|
||||
|
||||
return {CommandDispatchAction::UnsupportedTool, intent, unsupportedToolMessage(intent.type)};
|
||||
}
|
||||
|
||||
QString CommandDispatcher::unsupportedToolMessage(UserIntentType type) const
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case UserIntentType::Reminder:
|
||||
return QStringLiteral("提醒功能尚未接入,我现在还不能创建本地提醒。");
|
||||
case UserIntentType::Weather:
|
||||
return QStringLiteral("天气查询功能尚未接入,我现在还不能查询实时天气。");
|
||||
case UserIntentType::FileOperation:
|
||||
return QStringLiteral("本地文件操作功能尚未接入。为避免风险,我现在不会读写或删除文件。");
|
||||
case UserIntentType::Search:
|
||||
return QStringLiteral("联网搜索功能尚未接入,我现在不会发起搜索请求。");
|
||||
case UserIntentType::Chat:
|
||||
return QString();
|
||||
}
|
||||
|
||||
return QStringLiteral("该工具功能尚未接入。");
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include "IntentRouter.h"
|
||||
|
||||
#include <QString>
|
||||
|
||||
enum class CommandDispatchAction
|
||||
{
|
||||
Chat,
|
||||
UnsupportedTool,
|
||||
};
|
||||
|
||||
struct CommandDispatchResult
|
||||
{
|
||||
CommandDispatchAction action = CommandDispatchAction::Chat;
|
||||
UserIntent intent;
|
||||
QString message;
|
||||
};
|
||||
|
||||
class CommandDispatcher
|
||||
{
|
||||
public:
|
||||
CommandDispatchResult dispatch(const QString &text) const;
|
||||
|
||||
private:
|
||||
QString unsupportedToolMessage(UserIntentType type) const;
|
||||
|
||||
IntentRouter m_intentRouter;
|
||||
};
|
||||
@@ -0,0 +1,123 @@
|
||||
#include "IntentRouter.h"
|
||||
|
||||
#include <QStringList>
|
||||
#include <QtGlobal>
|
||||
|
||||
namespace
|
||||
{
|
||||
bool containsAny(const QString &text, const QStringList &keywords)
|
||||
{
|
||||
for (const QString &keyword : keywords)
|
||||
{
|
||||
if (text.contains(keyword, Qt::CaseInsensitive))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isReminderIntent(const QString &text)
|
||||
{
|
||||
static const QStringList keywords = {
|
||||
QStringLiteral("提醒"),
|
||||
QStringLiteral("提醒我"),
|
||||
QStringLiteral("叫我"),
|
||||
QStringLiteral("闹钟"),
|
||||
QStringLiteral("到点"),
|
||||
};
|
||||
return containsAny(text, keywords);
|
||||
}
|
||||
|
||||
bool isFileOperationIntent(const QString &text)
|
||||
{
|
||||
static const QStringList keywords = {
|
||||
QStringLiteral("文件"),
|
||||
QStringLiteral("文件夹"),
|
||||
QStringLiteral("目录"),
|
||||
QStringLiteral("保存到"),
|
||||
QStringLiteral("截图"),
|
||||
QStringLiteral("打包"),
|
||||
QStringLiteral("压缩"),
|
||||
QStringLiteral("复制"),
|
||||
QStringLiteral("备份"),
|
||||
QStringLiteral("重命名"),
|
||||
QStringLiteral("删除"),
|
||||
QStringLiteral("移动"),
|
||||
QStringLiteral("读取"),
|
||||
QStringLiteral(".txt"),
|
||||
QStringLiteral(".md"),
|
||||
QStringLiteral("日志"),
|
||||
};
|
||||
return containsAny(text, keywords);
|
||||
}
|
||||
|
||||
bool isWeatherIntent(const QString &text)
|
||||
{
|
||||
static const QStringList keywords = {
|
||||
QStringLiteral("天气"),
|
||||
QStringLiteral("气温"),
|
||||
QStringLiteral("温度"),
|
||||
QStringLiteral("冷不冷"),
|
||||
QStringLiteral("热不热"),
|
||||
QStringLiteral("下雨"),
|
||||
QStringLiteral("降雨"),
|
||||
QStringLiteral("带伞"),
|
||||
QStringLiteral("刮风"),
|
||||
QStringLiteral("风力"),
|
||||
QStringLiteral("湿度"),
|
||||
QStringLiteral("空气质量"),
|
||||
QStringLiteral("雾霾"),
|
||||
QStringLiteral("适合出门"),
|
||||
QStringLiteral("穿什么"),
|
||||
QStringLiteral("外面怎么样"),
|
||||
};
|
||||
return containsAny(text, keywords);
|
||||
}
|
||||
|
||||
bool isSearchIntent(const QString &text)
|
||||
{
|
||||
static const QStringList keywords = {
|
||||
QStringLiteral("搜索"),
|
||||
QStringLiteral("搜一下"),
|
||||
QStringLiteral("查一下"),
|
||||
QStringLiteral("查找"),
|
||||
QStringLiteral("联网"),
|
||||
QStringLiteral("最新"),
|
||||
QStringLiteral("官网"),
|
||||
QStringLiteral("新闻"),
|
||||
QStringLiteral("版本"),
|
||||
QStringLiteral("文档"),
|
||||
QStringLiteral("报错"),
|
||||
};
|
||||
return containsAny(text, keywords);
|
||||
}
|
||||
}
|
||||
|
||||
UserIntent IntentRouter::route(const QString &text) const
|
||||
{
|
||||
const QString trimmedText = text.trimmed();
|
||||
|
||||
if (isReminderIntent(trimmedText))
|
||||
{
|
||||
return {UserIntentType::Reminder, trimmedText};
|
||||
}
|
||||
|
||||
if (isFileOperationIntent(trimmedText))
|
||||
{
|
||||
return {UserIntentType::FileOperation, trimmedText};
|
||||
}
|
||||
|
||||
if (isWeatherIntent(trimmedText))
|
||||
{
|
||||
return {UserIntentType::Weather, trimmedText};
|
||||
}
|
||||
|
||||
if (isSearchIntent(trimmedText))
|
||||
{
|
||||
return {UserIntentType::Search, trimmedText};
|
||||
}
|
||||
|
||||
return {UserIntentType::Chat, trimmedText};
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include "UserIntent.h"
|
||||
|
||||
#include <QString>
|
||||
|
||||
class IntentRouter
|
||||
{
|
||||
public:
|
||||
UserIntent route(const QString &text) const;
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
|
||||
enum class UserIntentType
|
||||
{
|
||||
Chat,
|
||||
Reminder,
|
||||
Weather,
|
||||
FileOperation,
|
||||
Search,
|
||||
};
|
||||
|
||||
struct UserIntent
|
||||
{
|
||||
UserIntentType type = UserIntentType::Chat;
|
||||
QString text;
|
||||
};
|
||||
|
||||
QString userIntentTypeName(UserIntentType type);
|
||||
+31
-4
@@ -3,6 +3,7 @@
|
||||
#include "../ai/AIProviderFactory.h"
|
||||
#include "../ai/ConversationManager.h"
|
||||
#include "../ai/ConversationStore.h"
|
||||
#include "../assistant/CommandDispatcher.h"
|
||||
#include "../character/CharacterPackageLoader.h"
|
||||
#include "../character/CharacterPackageRepository.h"
|
||||
#include "../config/ConfigManager.h"
|
||||
@@ -460,19 +461,45 @@ bool PetWindow::submitChatMessage(const QString &message)
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString trimmedMessage = message.trimmed();
|
||||
if (trimmedMessage.isEmpty())
|
||||
const QString normalizedMessage = message.trimmed();
|
||||
if (normalizedMessage.isEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (trimmedMessage.size() > MaxUserMessageLength)
|
||||
if (normalizedMessage.size() > MaxUserMessageLength)
|
||||
{
|
||||
playState(QStringLiteral("error"), false);
|
||||
showBubbleMessage(QStringLiteral("消息太长,请控制在 4000 字以内。"));
|
||||
return false;
|
||||
}
|
||||
|
||||
CommandDispatcher dispatcher;
|
||||
const CommandDispatchResult result = dispatcher.dispatch(normalizedMessage);
|
||||
if (result.action == CommandDispatchAction::UnsupportedTool)
|
||||
{
|
||||
playState(QStringLiteral("talk"), false);
|
||||
showBubbleMessage(result.message);
|
||||
return true;
|
||||
}
|
||||
|
||||
return submitAiChatMessage(result.message);
|
||||
}
|
||||
|
||||
bool PetWindow::submitAiChatMessage(const QString &message)
|
||||
{
|
||||
if (!m_conversationManager || m_conversationManager->isBusy())
|
||||
{
|
||||
showBubbleMessage(QStringLiteral("AI 回复正在进行。"));
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString chatMessage = message.trimmed();
|
||||
if (chatMessage.isEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ConfigManager configManager;
|
||||
AIConfig config = configManager.loadAIConfig();
|
||||
QString errorMessage;
|
||||
@@ -507,7 +534,7 @@ bool PetWindow::submitChatMessage(const QString &message)
|
||||
|
||||
QPointer<PetWindow> window(this);
|
||||
m_conversationManager->sendUserMessageStreaming(
|
||||
trimmedMessage,
|
||||
chatMessage,
|
||||
[window](const QString &delta) {
|
||||
if (!window.isNull())
|
||||
{
|
||||
|
||||
@@ -60,6 +60,7 @@ private:
|
||||
void addStateTestActions(QMenu *menu);
|
||||
void startChat();
|
||||
bool submitChatMessage(const QString &message);
|
||||
bool submitAiChatMessage(const QString &message);
|
||||
void clearConversation();
|
||||
void cancelActiveAIRequest();
|
||||
void showConversationHistory();
|
||||
|
||||
Reference in New Issue
Block a user