Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ChatGPT like memories #113

Merged
merged 1 commit into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -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']
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ A third-party GPT Client for OpenAI API.

<!-- Badges-->
<p align="center">
<a href="https://ko-fi.com/lollipopkit"><img alt="donation" width="130" src="https://storage.ko-fi.com/cdn/brandasset/kofi_button_red.png"></a>
<img alt="lang" src="https://img.shields.io/badge/lang-dart-pink">
<img alt="license" src="https://img.shields.io/badge/license-GPLv3-pink">
</p>
Expand All @@ -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.
Expand Down Expand Up @@ -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`

11 changes: 9 additions & 2 deletions README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@

<!-- Badges-->
<p align="center">
<img alt="lang" src="https://img.shields.io/badge/lang-dart-pink">
<img alt="license" src="https://img.shields.io/badge/license-GPLv3-pink">
<a href="https://ko-fi.com/lollipopkit"><img alt="捐赠" width="130" src="https://storage.ko-fi.com/cdn/brandasset/kofi_button_red.png"></a>
<img alt="语言" src="https://img.shields.io/badge/lang-dart-pink">
<img alt="证书" src="https://img.shields.io/badge/license-GPLv3-pink">
</p>

## 😣 注意
Expand All @@ -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) 恢复
- 文本 / 图片 / 音频聊天
Expand Down Expand Up @@ -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`
6 changes: 4 additions & 2 deletions lib/core/util/tool_func/func/http.dart
Original file line number Diff line number Diff line change
@@ -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: '''
Expand Down
4 changes: 2 additions & 2 deletions lib/core/util/tool_func/func/iface.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});

Expand Down
43 changes: 43 additions & 0 deletions lib/core/util/tool_func/func/memory.dart
Original file line number Diff line number Diff line change
@@ -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))];
}
}
17 changes: 10 additions & 7 deletions lib/core/util/tool_func/tool.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<OpenAIToolModel> get tools {
final tools = <OpenAIToolModel>[];
if (!Stores.tool.enabled.fetch()) return tools;
final enabledTools = Stores.tool.enabledTools.fetch();
for (final tool in internalTools) {
if (enabledTools.contains(tool.name)) {
Expand All @@ -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}';
}
}
}
5 changes: 2 additions & 3 deletions lib/data/res/openai.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool> updateModels({bool force = false}) async {
if (current.url.startsWith('https://api.openai.com') &&
current.key.isEmpty) {
Expand Down
5 changes: 5 additions & 0 deletions lib/data/store/tool.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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', <String>[]);

/// 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', <String>[]);
}
16 changes: 9 additions & 7 deletions lib/intro.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
);
},
),
);
},
);
Expand Down
6 changes: 5 additions & 1 deletion lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -125,14 +129,14 @@
"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",
"themeColorSeed": "Theme color seed",
"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}.",
Expand Down
6 changes: 5 additions & 1 deletion lib/l10n/app_zh.arb
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,11 @@
"languageName": "简体中文",
"license": "许可证",
"licenseMenuItem": "开放源代码许可",
"list": "列表",
"manual": "手动",
"memory": "记忆",
"memoryAdded": "记忆已添加: {str}",
"memoryTip": "记住 [{txt}]?",
"message": "消息",
"minute": "分钟",
"model": "模型",
Expand Down Expand Up @@ -125,14 +129,14 @@
"stt": "语音转文字",
"success": "成功 🏅~",
"sureRestoreFmt": "确定恢复备份({time})?",
"switcher": "开关",
"syncConflict": "冲突:不能同时开启 {a} 和 {b}",
"system": "系统",
"text": "文字",
"themeColorSeed": "主题颜色种子",
"themeMode": "主题模式",
"thirdParty": "第三方",
"tool": "工具",
"toolAvailability": "仅 gpt-4o、gpt-4t 等支持",
"toolConfirmFmt": "是否同意使用工具 {tool} ?",
"toolFinishTip": "工具调用完成",
"toolHttpReqHelp": "将会与从网络获取数据,本次将会联系 {host}",
Expand Down
5 changes: 5 additions & 0 deletions lib/view/page/backup/impl/gpt_next.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,10 @@ void _onTapRestoreGPTNext(BuildContext context) async {
);
});

if (chats == null) {
context.showSnackBar('null');
return;
}

_askConfirm(context, chats);
}
5 changes: 5 additions & 0 deletions lib/view/page/backup/impl/openai.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,10 @@ void _onTapRestoreOpenAI(BuildContext context) async {
);
});

if (chats == null) {
context.showSnackBar('null');
return;
}

_askConfirm(context, chats);
}
2 changes: 1 addition & 1 deletion lib/view/page/home/history.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
6 changes: 3 additions & 3 deletions lib/view/page/home/home.dart
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,9 @@ class _HomePageState extends State<HomePage>

@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(),
Expand Down
9 changes: 5 additions & 4 deletions lib/view/page/home/req.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ Iterable<OpenAIChatCompletionChoiceMessageModel> _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;

Expand Down Expand Up @@ -134,7 +135,7 @@ Future<void> _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;
Expand All @@ -145,7 +146,7 @@ Future<void> _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);
Expand Down
Loading
Loading