From 83cf2dd31f10a500efca270b404c0a8d98448a7d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?lollipopkit=F0=9F=8F=B3=EF=B8=8F=E2=80=8D=E2=9A=A7?=
=?UTF-8?q?=EF=B8=8F?= <10864310+lollipopkit@users.noreply.github.com>
Date: Thu, 25 Jul 2024 23:33:03 +0800
Subject: [PATCH] feat: ChatGPT like `memories` (#113)
Fixes #110
---
.github/FUNDING.yml | 14 +++++
README.md | 9 ++++
README_zh.md | 11 +++-
lib/core/util/tool_func/func/http.dart | 6 ++-
lib/core/util/tool_func/func/iface.dart | 4 +-
lib/core/util/tool_func/func/memory.dart | 43 +++++++++++++++
lib/core/util/tool_func/tool.dart | 17 +++---
lib/data/res/openai.dart | 5 +-
lib/data/store/tool.dart | 5 ++
lib/intro.dart | 16 +++---
lib/l10n/app_en.arb | 6 ++-
lib/l10n/app_zh.arb | 6 ++-
lib/view/page/backup/impl/gpt_next.dart | 5 ++
lib/view/page/backup/impl/openai.dart | 5 ++
lib/view/page/home/history.dart | 2 +-
lib/view/page/home/home.dart | 6 +--
lib/view/page/home/req.dart | 9 ++--
lib/view/page/tool.dart | 66 ++++++++++++++++--------
pubspec.lock | 4 +-
pubspec.yaml | 2 +-
20 files changed, 184 insertions(+), 57 deletions(-)
create mode 100644 .github/FUNDING.yml
create mode 100644 lib/core/util/tool_func/func/memory.dart
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..a41794d
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,14 @@
+# These are supported funding model platforms
+
+github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
+patreon: # Replace with a single Patreon username
+open_collective: # Replace with a single Open Collective username
+ko_fi: lollipopkit # Replace with a single Ko-fi username
+tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
+community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
+liberapay: # Replace with a single Liberapay username
+issuehunt: # Replace with a single IssueHunt username
+lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
+polar: # Replace with a single Polar username
+buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
+custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
diff --git a/README.md b/README.md
index c8a2138..401878e 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,7 @@ A third-party GPT Client for OpenAI API.
+
@@ -18,6 +19,7 @@ Please refrain from using it in production environments or for critical data.
## 🪄 Features
+- (🥳 New) Ask GPT to add Memories.
- (🥳 New) Api supports viewing the content of Http links, (developing) running JS scripts locally. [Video](https://cdn.lolli.tech/gptbox/screenshot/tools.mp4)
- Restore from [ChatGPT Next Web backup](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web) / [OpenAI exported file](https://chatgpt.com).
- Text / Image / Audio chat.
@@ -68,10 +70,17 @@ After you read the above, you can:
- Any positive contribution is welcome.
- [l10n guide](https://blog.lolli.tech/faq/) can be found in my blog.
+
## 💡 My other apps
- [Server Box](https://github.com/lollipopkit/flutter_server_box) - Server status & tools.
- [More](https://github.com/lollipopkit) - Tools & etc.
+
+## 🎉 Donation
+- App will keep free, but if you think my work is helpful, you can [donate](https://ko-fi.com/lollipopkit) me a cup of coffee.
+- Thanks to all the donators, your support is my motivation.
+
+
## 📝 License
`GPL v3 lollipopkit`
diff --git a/README_zh.md b/README_zh.md
index 7dd5e78..f998aa8 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -8,8 +8,9 @@
-
-
+
+
+
## 😣 注意
@@ -18,6 +19,7 @@
## 🪄 特性
+- (🥳 新) 让 GPT 记住某些事
- (🥳 新) Api 支持查看 Http 链接的内容、(开发中)本地运行 JS 脚本。[视频](https://cdn.lolli.tech/gptbox/screenshot/tools.mp4)
- 从 [ChatGPT Next Web 备份](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web) / [OpenAI导出文件](https://chatgpt.com) 恢复
- 文本 / 图片 / 音频聊天
@@ -77,5 +79,10 @@ Linux & Windows | [Github](https://github.com/lollipopkit/flutter_gpt_box/releas
- [更多](https://github.com/lollipopkit) - 工具 & etc.
+## 🎉 捐赠
+- 应用将保持免费,但是如果你认为我的工作对你有帮助,欢迎[捐赠](https://ko-fi.com/lollipopkit)。
+- 感谢所有捐赠者,你们的支持是我的动力。
+
+
## 📝 协议
`GPL v3 lollipopkit`
diff --git a/lib/core/util/tool_func/func/http.dart b/lib/core/util/tool_func/func/http.dart
index 8d315a4..556cb8a 100644
--- a/lib/core/util/tool_func/func/http.dart
+++ b/lib/core/util/tool_func/func/http.dart
@@ -1,7 +1,9 @@
part of '../tool.dart';
-final class _HttpReq extends ToolFunc {
- const _HttpReq()
+final class TfHttpReq extends ToolFunc {
+ static const instance = TfHttpReq._();
+
+ const TfHttpReq._()
: super(
name: 'httpReq',
description: '''
diff --git a/lib/core/util/tool_func/func/iface.dart b/lib/core/util/tool_func/func/iface.dart
index ca1752f..70e3b0f 100644
--- a/lib/core/util/tool_func/func/iface.dart
+++ b/lib/core/util/tool_func/func/iface.dart
@@ -2,12 +2,12 @@ part of '../tool.dart';
abstract final class ToolFunc {
final String name;
- final String? description;
+ final String description;
final _Map parametersSchema;
const ToolFunc({
required this.name,
- this.description,
+ required this.description,
required this.parametersSchema,
});
diff --git a/lib/core/util/tool_func/func/memory.dart b/lib/core/util/tool_func/func/memory.dart
new file mode 100644
index 0000000..8de2474
--- /dev/null
+++ b/lib/core/util/tool_func/func/memory.dart
@@ -0,0 +1,43 @@
+part of '../tool.dart';
+
+final class TfMemory extends ToolFunc {
+ static const instance = TfMemory._();
+
+ const TfMemory._()
+ : super(
+ name: 'memory',
+ description: '''
+Memorise the input and add what memorised to the prompt.
+If users want to memorise something, you(AI models) should call this function.''',
+ parametersSchema: const {
+ 'type': 'object',
+ 'properties': {
+ 'memory': {
+ 'type': 'string',
+ 'description': 'What to memorise, will be persisted in db.',
+ },
+ },
+ },
+ );
+
+ @override
+ String get l10nName => l10n.memory;
+
+ @override
+ String help(_CallResp call, _Map args) {
+ return l10n.memoryTip(args['memory'] as String? ?? '>');
+ }
+
+ @override
+ Future<_Ret> run(_CallResp call, _Map args, OnToolLog log) async {
+ final memory = args['memory'] as String?;
+ if (memory == null) {
+ return [ChatContent.text(l10n.empty)];
+ }
+ final prop = Stores.tool.memories;
+ final memories = prop.fetch();
+ prop.put(memories..add(memory));
+ await Future.delayed(Durations.medium1);
+ return [ChatContent.text(l10n.memoryAdded(memory))];
+ }
+}
diff --git a/lib/core/util/tool_func/tool.dart b/lib/core/util/tool_func/tool.dart
index 9b4dd07..0731628 100644
--- a/lib/core/util/tool_func/tool.dart
+++ b/lib/core/util/tool_func/tool.dart
@@ -14,15 +14,18 @@ part 'type.dart';
part 'func/iface.dart';
part 'func/http.dart';
part 'func/js.dart';
+part 'func/memory.dart';
abstract final class OpenAIFuncCalls {
static const internalTools = [
- _HttpReq(),
+ TfHttpReq.instance,
+ TfMemory.instance,
//_RunJS(),
];
static List get tools {
final tools = [];
+ if (!Stores.tool.enabled.fetch()) return tools;
final enabledTools = Stores.tool.enabledTools.fetch();
for (final tool in internalTools) {
if (enabledTools.contains(tool.name)) {
@@ -37,17 +40,17 @@ abstract final class OpenAIFuncCalls {
ToolConfirm askConfirm,
OnToolLog onToolLog,
) async {
- final tool = tools.firstWhere((t) => t.type == resp.type);
- switch (tool.type) {
+ switch (resp.type) {
case 'function':
- final fn = tool.function;
+ final targetName = resp.function.name;
+ final func =
+ internalTools.firstWhereOrNull((e) => e.name == targetName);
+ if (func == null) throw 'Unknown function $targetName';
final args = await _parseMap(resp.function.arguments);
- final func = internalTools.firstWhereOrNull((e) => e.name == fn.name);
- if (func == null) throw 'Unknown function ${fn.name}';
if (!await askConfirm(func, func.help(resp, args))) return null;
return await func.run(resp, args, onToolLog);
default:
- throw 'Unknown tool type ${tool.type}';
+ throw 'Unknown tool type ${resp.type}';
}
}
}
diff --git a/lib/data/res/openai.dart b/lib/data/res/openai.dart
index 27a90d3..5b9a7e8 100644
--- a/lib/data/res/openai.dart
+++ b/lib/data/res/openai.dart
@@ -36,13 +36,12 @@ abstract final class OpenAICfg {
}
static RegExp? _modelsUseToolReExp;
- static bool canUseTool(String model) {
+ static bool isToolCompatible({String? model}) {
+ model ??= current.model;
if (model.isEmpty) return false;
return _modelsUseToolReExp?.hasMatch(model) ?? false;
}
- static bool get canUseToolNow => canUseTool(current.model);
-
static Future updateModels({bool force = false}) async {
if (current.url.startsWith('https://api.openai.com') &&
current.key.isEmpty) {
diff --git a/lib/data/store/tool.dart b/lib/data/store/tool.dart
index 804bd29..6728738 100644
--- a/lib/data/store/tool.dart
+++ b/lib/data/store/tool.dart
@@ -17,4 +17,9 @@ final class ToolStore extends PersistentStore {
/// Tools that are permitted to be used by the user.
/// A dialog will be shown if the tool has not been permitted.
late final permittedTools = property('permittedTools', []);
+
+ /// Memories that are saved by the user.
+ /// It will be added to prompt when sending a chat req.
+ /// {id: memory}
+ late final memories = property('memories', []);
}
diff --git a/lib/intro.dart b/lib/intro.dart
index 3cd0beb..c988efd 100644
--- a/lib/intro.dart
+++ b/lib/intro.dart
@@ -18,13 +18,15 @@ final class _IntroPage extends StatelessWidget {
final padTop = cons.maxHeight * .12;
final pages_ = pages.map((e) => e(context, padTop)).toList();
return IntroPage(
- pages: pages_,
- onDone: (ctx) {
- Stores.setting.introVer.put(Build.build);
- Navigator.of(ctx).pushReplacement(
- MaterialPageRoute(builder: (_) => const HomePage()),
- );
- },
+ args: IntroPageArgs(
+ pages: pages_,
+ onDone: (ctx) {
+ Stores.setting.introVer.put(Build.build);
+ Navigator.of(ctx).pushReplacement(
+ MaterialPageRoute(builder: (_) => const HomePage()),
+ );
+ },
+ ),
);
},
);
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index c26fd8b..cc3bfb3 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -74,7 +74,11 @@
"languageName": "English",
"license": "License",
"licenseMenuItem": "Open-source licenses",
+ "list": "List",
"manual": "Manual",
+ "memory": "Memory",
+ "memoryAdded": "Memory added: {str}",
+ "memoryTip": "Memorise [{txt}]?",
"message": "Message",
"minute": "min",
"model": "Model",
@@ -125,6 +129,7 @@
"stt": "stt",
"success": "Success 🏅 ~",
"sureRestoreFmt": "Are you sure to restore Backup({time})?",
+ "switcher": "Switch",
"syncConflict": "Sync conflict: can't turn on {a} and {b} at the same time.",
"system": "System",
"text": "Text",
@@ -132,7 +137,6 @@
"themeMode": "Theme mode",
"thirdParty": "Third Party",
"tool": "Tool",
- "toolAvailability": "Only on gpt-4o, gpt-4t & etc.",
"toolConfirmFmt": "Is it permitted to use the tool {tool} ?",
"toolFinishTip": "Tools invocation completed",
"toolHttpReqHelp": "It will fetch data from network. In this time, it will communicate with {host}.",
diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb
index 5e34723..76bcbbd 100644
--- a/lib/l10n/app_zh.arb
+++ b/lib/l10n/app_zh.arb
@@ -74,7 +74,11 @@
"languageName": "简体中文",
"license": "许可证",
"licenseMenuItem": "开放源代码许可",
+ "list": "列表",
"manual": "手动",
+ "memory": "记忆",
+ "memoryAdded": "记忆已添加: {str}",
+ "memoryTip": "记住 [{txt}]?",
"message": "消息",
"minute": "分钟",
"model": "模型",
@@ -125,6 +129,7 @@
"stt": "语音转文字",
"success": "成功 🏅~",
"sureRestoreFmt": "确定恢复备份({time})?",
+ "switcher": "开关",
"syncConflict": "冲突:不能同时开启 {a} 和 {b}",
"system": "系统",
"text": "文字",
@@ -132,7 +137,6 @@
"themeMode": "主题模式",
"thirdParty": "第三方",
"tool": "工具",
- "toolAvailability": "仅 gpt-4o、gpt-4t 等支持",
"toolConfirmFmt": "是否同意使用工具 {tool} ?",
"toolFinishTip": "工具调用完成",
"toolHttpReqHelp": "将会与从网络获取数据,本次将会联系 {host}",
diff --git a/lib/view/page/backup/impl/gpt_next.dart b/lib/view/page/backup/impl/gpt_next.dart
index 164de23..6397b4c 100644
--- a/lib/view/page/backup/impl/gpt_next.dart
+++ b/lib/view/page/backup/impl/gpt_next.dart
@@ -37,5 +37,10 @@ void _onTapRestoreGPTNext(BuildContext context) async {
);
});
+ if (chats == null) {
+ context.showSnackBar('null');
+ return;
+ }
+
_askConfirm(context, chats);
}
diff --git a/lib/view/page/backup/impl/openai.dart b/lib/view/page/backup/impl/openai.dart
index 7942ec6..f6bb686 100644
--- a/lib/view/page/backup/impl/openai.dart
+++ b/lib/view/page/backup/impl/openai.dart
@@ -38,5 +38,10 @@ void _onTapRestoreOpenAI(BuildContext context) async {
);
});
+ if (chats == null) {
+ context.showSnackBar('null');
+ return;
+ }
+
_askConfirm(context, chats);
}
diff --git a/lib/view/page/home/history.dart b/lib/view/page/home/history.dart
index 04ff812..f636887 100644
--- a/lib/view/page/home/history.dart
+++ b/lib/view/page/home/history.dart
@@ -89,7 +89,7 @@ class _HistoryPageState extends State<_HistoryPage>
children: [
IconBtn(
onTap: () => _onTapRenameChat(chatId, context),
- icon: Icons.abc,
+ icon: BoxIcons.bx_rename,
),
IconBtn(
onTap: () => _onTapDeleteChat(chatId, context),
diff --git a/lib/view/page/home/home.dart b/lib/view/page/home/home.dart
index 407cef1..a9bc765 100644
--- a/lib/view/page/home/home.dart
+++ b/lib/view/page/home/home.dart
@@ -86,9 +86,9 @@ class _HomePageState extends State
@override
Widget build(BuildContext context) {
- return const ExitConfirm(
- onPop: ExitConfirm.exitApp,
- child: Scaffold(
+ return ExitConfirm(
+ onPop: (_) => ExitConfirm.exitApp(),
+ child: const Scaffold(
drawer: _Drawer(),
appBar: _CustomAppBar(),
body: _Body(),
diff --git a/lib/view/page/home/req.dart b/lib/view/page/home/req.dart
index c357b04..89cd4d1 100644
--- a/lib/view/page/home/req.dart
+++ b/lib/view/page/home/req.dart
@@ -27,10 +27,11 @@ Iterable _historyCarried(
final ignoreCtxCons = workingChat.settings?.ignoreContextConstraint == true;
if (ignoreCtxCons) return workingChat.items.map((e) => e.toOpenAI);
- final prompt = config.prompt.isNotEmpty
+ final promptStr = config.prompt + Stores.tool.memories.fetch().join('\n');
+ final prompt = promptStr.isNotEmpty
? ChatHistoryItem.single(
role: ChatRole.system,
- raw: config.prompt,
+ raw: promptStr,
).toOpenAI
: null;
@@ -134,7 +135,7 @@ Future _onCreateText(
_loadingChatIds.add(chatId);
_autoScroll(chatId);
- final useTools = Stores.tool.enabled.fetch() && OpenAICfg.canUseToolNow;
+ final toolCompatible = OpenAICfg.isToolCompatible();
// #104
final singleChatScopeUseTools = workingChat.settings?.useTools != false;
@@ -145,7 +146,7 @@ Future _onCreateText(
/// TODO: after switching to http img url, remove this condition.
/// To save tokens, we don't use tools for image prompt
- if (useTools && !hasImg && singleChatScopeUseTools && !isToolsEmpty) {
+ if (toolCompatible && !hasImg && singleChatScopeUseTools && !isToolsEmpty) {
final toolReply = ChatHistoryItem.single(role: ChatRole.tool, raw: '');
workingChat.items.add(toolReply);
_loadingChatIds.add(toolReply.id);
diff --git a/lib/view/page/tool.dart b/lib/view/page/tool.dart
index 7a56805..0f00062 100644
--- a/lib/view/page/tool.dart
+++ b/lib/view/page/tool.dart
@@ -31,13 +31,42 @@ class _ToolPageState extends State {
children: [
_buildUseTool(),
_buildModelRegExp(),
- _buildTitle('~'),
- _buildSwicthes(),
+ _buildTitle(l10n.list),
+ _buildSwitchTile(TfHttpReq.instance).cardx,
+ _buildMemory(),
const SizedBox(height: 37),
],
);
}
+ Widget _buildMemory() {
+ return ExpandTile(
+ title: Text(l10n.memory),
+ children: [
+ _buildSwitchTile(TfMemory.instance, title: l10n.switcher),
+ ListTile(
+ title: Text(l10n.edit),
+ onTap: () async {
+ final data = _store.memories.fetch();
+ final dataMap = {};
+ for (var idx = 0; idx < data.length; idx++) {
+ dataMap['$idx'] = data[idx];
+ }
+ final res = await KvEditor.route.go(
+ context,
+ args: KvEditorArgs(data: dataMap),
+ );
+ if (res != null) {
+ _store.memories.put(res.values.toList());
+ context.showSnackBar(l10n.success);
+ }
+ },
+ trailing: const Icon(Icons.keyboard_arrow_right),
+ ),
+ ],
+ ).cardx;
+ }
+
Widget _buildTitle(String text) {
return Padding(
padding: const EdgeInsets.only(top: 23, bottom: 17),
@@ -53,8 +82,7 @@ class _ToolPageState extends State {
Widget _buildUseTool() {
return ListTile(
leading: const Icon(MingCute.tool_line),
- title: Text(l10n.tool),
- subtitle: Text(l10n.toolAvailability, style: UIs.textGrey),
+ title: Text(l10n.switcher),
trailing: StoreSwitch(prop: _store.enabled),
).cardx;
}
@@ -96,27 +124,23 @@ class _ToolPageState extends State {
).cardx;
}
- Widget _buildSwicthes() {
+ Widget _buildSwitchTile(ToolFunc e, {String? title}) {
final prop = _store.enabledTools;
return ValBuilder(
listenable: prop.listenable(),
builder: (vals) {
- return Column(
- children: OpenAIFuncCalls.internalTools.map((e) {
- return ListTile(
- title: Text(e.name),
- subtitle: Text(e.l10nName),
- trailing: Switch(
- value: vals.contains(e.name),
- onChanged: (val) {
- final _ = switch (val) {
- true => prop.put(vals..add(e.name)),
- false => prop.put(vals..remove(e.name)),
- };
- },
- ),
- );
- }).toList(),
+ final name = e.name;
+ return ListTile(
+ title: Text(title ?? e.l10nName),
+ trailing: Switch(
+ value: vals.contains(name),
+ onChanged: (val) {
+ final _ = switch (val) {
+ true => prop.put(vals..add(name)),
+ false => prop.put(vals..remove(name)),
+ };
+ },
+ ),
);
},
);
diff --git a/pubspec.lock b/pubspec.lock
index 457571d..e20de35 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -407,8 +407,8 @@ packages:
dependency: "direct main"
description:
path: "."
- ref: "v1.0.79"
- resolved-ref: f1bc7dd5ec2af84813b33a9e2149e117dfea3cd0
+ ref: "v1.0.88"
+ resolved-ref: fd87becb3c112edac090903e78d863b5ca93a70e
url: "https://github.com/lppcg/fl_lib"
source: git
version: "0.0.1"
diff --git a/pubspec.yaml b/pubspec.yaml
index a2cf701..7159d1b 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -36,7 +36,7 @@ dependencies:
fl_lib:
git:
url: https://github.com/lppcg/fl_lib
- ref: v1.0.79
+ ref: v1.0.88
dependency_overrides:
# fl_lib: