better_prompt 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1b0275e59070ddcb50314443ed257f616af962ab8e7e328ee78ff337ef106b37
4
+ data.tar.gz: 6e2a503dfa2399a1ddf25276647edf7a85422b723aeaf444c01832869d504e8b
5
+ SHA512:
6
+ metadata.gz: e14e38212b0ac9b0840647f37acac1f61602e9a5152bce1dff23be3c2836bb658fb5a4c50434be1d1d9a22c9aaecfe0e16ab4f71467ebc35772996679c852117
7
+ data.tar.gz: 2614387d896abbfe25104ed50e38c2752fc363d3a876d47368c6f7ea4afb55bc5677f5f56a7a819df9721f61a52db453e83346a887d096648bc4965ec7e6a858
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 庄表伟
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.cn.md ADDED
@@ -0,0 +1,107 @@
1
+ # BetterPrompt: 命令行中的 AI 提示词管理利器
2
+
3
+ BetterPrompt 是一个功能强大的命令行工具,旨在帮助开发者和内容创作者高效地管理、测试和调用 AI 模型的提示词(Prompts)。
4
+
5
+ ## 🚀 功能特性
6
+
7
+ * **提示词管理**: 轻松实现提示词的增、删、改、查。
8
+ * **模板支持**: 创建和管理提示词模板,提高复用性。
9
+ * **模型调用**: 直接在命令行中调用 AI 模型并查看结果。
10
+ * **本地存储**: 使用 SQLite 数据库在本地安全地存储您的提示词和历史记录。
11
+ * **灵活配置**: 支持自定义 AI 模型的 API 地址和密钥。
12
+
13
+ ## 📦 安装
14
+
15
+ 通过 RubyGems 安装 BetterPrompt:
16
+
17
+ ```bash
18
+ gem install better_prompt
19
+ ```
20
+
21
+ ## ⚙️ 配置
22
+
23
+ 首次运行时,BetterPrompt 会引导您完成初始化。您也可以手动执行:
24
+
25
+ ```bash
26
+ bp --init
27
+ ```
28
+
29
+ 该命令会在您的用户主目录下创建 `.better_prompt` 文件夹,并包含 `config.yml` 配置文件和 `prompt.db` 数据库文件。
30
+
31
+ 请修改 `config.yml` 文件,填入您的 AI 模型提供商的 `api_key` 和 `api_url`。
32
+
33
+ ```yaml
34
+ # ~/.better_prompt/config.yml
35
+ api_key: "YOUR_API_KEY"
36
+ api_url: "https://api.openai.com/v1/chat/completions"
37
+ model: "gpt-3.5-turbo"
38
+ ```
39
+
40
+ ## 🛠️ 使用方法
41
+
42
+ BetterPrompt 提供了简洁的命令行接口来管理您的提示词。
43
+
44
+ ### 主要命令
45
+
46
+ * **列出所有提示词**
47
+ ```bash
48
+ bp --list
49
+ # or
50
+ bp -l
51
+ ```
52
+
53
+ * **显示一个提示词的详细内容**
54
+ ```bash
55
+ bp --show <ID>
56
+ # or
57
+ bp -s <ID>
58
+ ```
59
+
60
+ * **添加一个新的提示词**
61
+ *BetterPrompt 会启动您默认的文本编辑器来编写提示词内容。*
62
+ ```bash
63
+ bp --add
64
+ # or
65
+ bp -a
66
+ ```
67
+
68
+ * **编辑一个已存在的提示词**
69
+ *BetterPrompt 会启动您默认的文本编辑器来修改提示词内容。*
70
+ ```bash
71
+ bp --edit <ID>
72
+ # or
73
+ bp -e <ID>
74
+ ```
75
+
76
+ * **删除一个提示词**
77
+ ```bash
78
+ bp --delete <ID>
79
+ # or
80
+ bp -d <ID>
81
+ ```
82
+
83
+ * **调用 AI 模型**
84
+ 使用指定的提示词 ID 来调用在 `config.yml` 中配置的 AI 模型。
85
+ ```bash
86
+ bp --call <ID>
87
+ ```
88
+
89
+ * **提供反馈**
90
+ 打开一个链接,为项目提供反馈。
91
+ ```bash
92
+ bp --feedback
93
+ ```
94
+
95
+ ## 👨‍💻 开发
96
+
97
+ 如果您想为 BetterPrompt 贡献代码,请遵循以下步骤:
98
+
99
+ 1. **Fork** 本项目。
100
+ 2. 创建您的功能分支 (`git checkout -b feature/AmazingFeature`)。
101
+ 3. 提交您的更改 (`git commit -m 'Add some AmazingFeature'`)。
102
+ 4. 将代码推送到分支 (`git push origin feature/AmazingFeature`)。
103
+ 5. 开启一个 **Pull Request**。
104
+
105
+ ## 📄 许可证
106
+
107
+ 本项目采用 MIT 许可证。详情请见 `LICENSE` 文件。
data/README.md ADDED
@@ -0,0 +1,100 @@
1
+ # Better Prompt
2
+
3
+ A Ruby Gem for tracking and analyzing LLM prompt interactions, designed to work with [smart_prompt](https://github.com/zhuangbiaowei/smart_prompt).
4
+
5
+ ## Features
6
+
7
+ - Records complete prompt/response history including:
8
+ - Prompt content and length
9
+ - Response content, length and timing
10
+ - Model information (name, provider, size)
11
+ - Call parameters (temperature, max_tokens, etc.)
12
+ - Stream vs non-stream calls
13
+ - Stores user feedback on response quality:
14
+ - Star ratings (1-5)
15
+ - Detailed scores (accuracy, relevance, etc.)
16
+ - Free-form comments
17
+ - Organizes prompts with:
18
+ - Tags and categories
19
+ - Projects grouping
20
+ - SQLite backend for easy analysis
21
+ - Character interface for analyzing logs and comparing responses:
22
+ - Execute `better_prompt ./path/database.db` to launch
23
+ - View interaction logs and history
24
+ - Compare different model responses side-by-side
25
+ - Evaluate and rate response quality
26
+ - Built with [ruby_rich](https://github.com/zhuangbiaowei/ruby_rich) for rich terminal UI
27
+
28
+ ## Database Schema
29
+
30
+ The database contains these main tables:
31
+
32
+ 1. `users` - User accounts
33
+ 2. `models` - LLM model information
34
+ 3. `prompts` - Prompt content and metadata
35
+ 4. `model_calls` - Individual API calls
36
+ 5. `responses` - Model responses
37
+ 6. `feedback` - User ratings and comments
38
+ 7. `tags`/`prompt_tags` - Prompt categorization
39
+ 8. `projects`/`project_prompts` - Prompt organization
40
+
41
+ See [db/init.sql](db/init.sql) for complete schema.
42
+
43
+ ## Installation
44
+
45
+ Add to your Gemfile:
46
+
47
+ ```ruby
48
+ gem 'better_prompt'
49
+ ```
50
+
51
+ Then execute:
52
+
53
+ ```bash
54
+ bundle install
55
+ ```
56
+
57
+ ## Usage
58
+
59
+ Basic setup:
60
+
61
+ ```ruby
62
+ require 'better_prompt'
63
+
64
+ # Initialize with database path
65
+ BetterPrompt.setup(db_path: 'path/to/database.db')
66
+
67
+ # Record a prompt and response
68
+ BetterPrompt.record(
69
+ user_id: 1,
70
+ prompt: "Explain quantum computing",
71
+ response: "Quantum computing uses qubits...",
72
+ model_name: "gpt-4",
73
+ response_time_ms: 1250,
74
+ is_stream: false
75
+ )
76
+
77
+ # Add user feedback
78
+ BetterPrompt.add_feedback(
79
+ response_id: 123,
80
+ rating: 4,
81
+ comment: "Helpful but could be more detailed"
82
+ )
83
+ ```
84
+
85
+ ## Development
86
+
87
+ After checking out the repo:
88
+
89
+ ```bash
90
+ bin/setup
91
+ bundle exec rake test
92
+ ```
93
+
94
+ ## Contributing
95
+
96
+ Bug reports and pull requests are welcome.
97
+
98
+ ## License
99
+
100
+ The gem is available as open source under MIT License.
data/bin/bp ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'better_prompt'
4
+
5
+ if ARGV[0]
6
+ db_path = ARGV[0]
7
+ else
8
+ db_path = "./db/prompt.db"
9
+ end
10
+ db_path = File.expand_path(db_path)
11
+ BetterPrompt.setup(db_path: db_path)
12
+
13
+ BetterPrompt::CLI.start(db_path)
14
+
data/db/init.sql ADDED
@@ -0,0 +1,83 @@
1
+ -- 创建模型表
2
+ CREATE TABLE models (
3
+ model_id INTEGER PRIMARY KEY AUTOINCREMENT,
4
+ model_provider TEXT NOT NULL,
5
+ model_name TEXT NOT NULL,
6
+ model_version TEXT,
7
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
8
+ );
9
+
10
+ -- 创建提示词表
11
+ CREATE TABLE prompts (
12
+ prompt_id INTEGER PRIMARY KEY AUTOINCREMENT,
13
+ role TEXT NOT NULL,
14
+ prompt_template_name TEXT NOT NULL,
15
+ prompt_title TEXT,
16
+ prompt_content TEXT NOT NULL,
17
+ prompt_length INTEGER NOT NULL,
18
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
19
+ );
20
+
21
+ -- 创建调用记录表
22
+ CREATE TABLE model_calls (
23
+ call_id INTEGER PRIMARY KEY AUTOINCREMENT,
24
+ prompt_list TEXT NOT NULL,
25
+ model_id INTEGER,
26
+ is_streaming INTEGER NOT NULL, -- 在SQLite中使用0/1代替布尔值
27
+ temperature REAL,
28
+ max_tokens INTEGER,
29
+ top_p REAL,
30
+ top_k INTEGER,
31
+ additional_parameters TEXT, -- 使用JSON字符串存储
32
+ call_timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
33
+ FOREIGN KEY (model_id) REFERENCES models(model_id)
34
+ );
35
+
36
+ -- 创建响应表
37
+ CREATE TABLE responses (
38
+ response_id INTEGER PRIMARY KEY AUTOINCREMENT,
39
+ call_id INTEGER,
40
+ response_content TEXT NOT NULL,
41
+ response_length INTEGER NOT NULL,
42
+ response_time_ms INTEGER NOT NULL, -- 响应时间(毫秒)
43
+ is_streaming INTEGER NOT NULL, -- 在SQLite中使用0/1代替布尔值
44
+ token_count INTEGER, -- 令牌数量
45
+ cost REAL, -- 调用成本
46
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
47
+ FOREIGN KEY (call_id) REFERENCES model_calls(call_id)
48
+ );
49
+
50
+ -- 创建用户评价表
51
+ CREATE TABLE feedback (
52
+ feedback_id INTEGER PRIMARY KEY AUTOINCREMENT,
53
+ response_id INTEGER,
54
+ rating INTEGER CHECK (rating BETWEEN 1 AND 5), -- 1-5星评价
55
+ feedback_comment TEXT,
56
+ accuracy_score INTEGER CHECK (accuracy_score BETWEEN 1 AND 10),
57
+ relevance_score INTEGER CHECK (relevance_score BETWEEN 1 AND 10),
58
+ creativity_score INTEGER CHECK (creativity_score BETWEEN 1 AND 10),
59
+ helpfulness_score INTEGER CHECK (helpfulness_score BETWEEN 1 AND 10),
60
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
61
+ FOREIGN KEY (response_id) REFERENCES responses(response_id)
62
+ );
63
+
64
+ -- 创建标签表,用于更好地分类提示词
65
+ CREATE TABLE tags (
66
+ tag_id INTEGER PRIMARY KEY AUTOINCREMENT,
67
+ tag_name TEXT NOT NULL UNIQUE,
68
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
69
+ );
70
+
71
+ -- 创建提示词-标签关联表
72
+ CREATE TABLE prompt_tags (
73
+ prompt_id INTEGER,
74
+ tag_id INTEGER,
75
+ PRIMARY KEY (prompt_id, tag_id),
76
+ FOREIGN KEY (prompt_id) REFERENCES prompts(prompt_id),
77
+ FOREIGN KEY (tag_id) REFERENCES tags(tag_id)
78
+ );
79
+
80
+ -- 创建索引以提高查询性能
81
+ CREATE INDEX idx_responses_call_id ON responses(call_id);
82
+ CREATE INDEX idx_feedback_response_id ON feedback(response_id);
83
+ CREATE INDEX idx_prompt_tags_prompt_id ON prompt_tags(prompt_id);
@@ -0,0 +1,27 @@
1
+ require "ruby_rich"
2
+
3
+ module BetterPrompt
4
+ class CLI
5
+ class << self
6
+ def start(db_path)
7
+ @layout = Component::Root.build
8
+ @layout.split_row(
9
+ Component::Sidebar.build,
10
+ Component::Main.build
11
+ )
12
+ @layout["sidebar"].split_column(
13
+ Component::TemplateList.build,
14
+ Component::PromptList.build,
15
+ )
16
+ @layout["main"].split_column(
17
+ Component::ModelCall.build,
18
+ Component::Response.build
19
+ )
20
+ ORM.setup("sqlite://"+db_path)
21
+ RubyRich::Live.start(@layout, refresh_rate: 24) do |live|
22
+ live.listening = true
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,33 @@
1
+ module BetterPrompt
2
+ module Component
3
+ class Feedback
4
+ def self.build(live)
5
+ dialog = RubyRich::Dialog.new(title: "加载对话", content: "请问这个回答的内容打分,1~5 之间。", width: 80, height: 20, buttons: [:cancel])
6
+ dialog.live = live
7
+ register_event_listener(dialog)
8
+ return dialog
9
+ end
10
+ def self.register_event_listener(dialog)
11
+ dialog.key(:escape){|event, live|
12
+ live.layout.hide_dialog
13
+ }
14
+ dialog.key(:string){|event, live|
15
+ key_value = event[:value].to_i
16
+ if key_value >=1 && key_value <=5
17
+ if feedback=live.params[:feedback]
18
+ feedback.rating = key_value
19
+ feedback.save
20
+ else
21
+ feedback = ORM::Feedback.new(response_id: live.params[:response_id], rating: key_value)
22
+ feedback.save
23
+ live.params[:feedback] = feedback
24
+ end
25
+ response = live.find_panel("response")
26
+ response.title = "回答 (Ctrl+r/Ctrl+a/Ctrl+t) 评分: #{key_value}"
27
+ live.layout.hide_dialog
28
+ end
29
+ }
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,10 @@
1
+ module BetterPrompt
2
+ module Component
3
+ class Main
4
+ def self.build
5
+ layout = RubyRich::Layout.new(name: "main", ratio: 3)
6
+ return layout
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,159 @@
1
+ module BetterPrompt
2
+ module Component
3
+ class ModelCall
4
+ def self.build
5
+ @current_pos = 0
6
+ layout = RubyRich::Layout.new(name: "model_call", size: 14)
7
+ draw_view(layout)
8
+ register_event_listener(layout)
9
+ return layout
10
+ end
11
+
12
+ def self.draw_view(layout)
13
+ layout.update_content(RubyRich::Panel.new(
14
+ "",
15
+ title: "模型调用 (Shift+3) 打分 (Ctrl+f)"
16
+ ))
17
+ end
18
+
19
+ def self.update_model_call_list(live, prompt_id)
20
+ model_call = live.find_panel("model_call")
21
+ @call_list = ORM::ModelCall.where_all(Sequel.like(:prompt_list, "%,#{prompt_id}]"))
22
+ @page_count = (@call_list.size / 9.0).ceil
23
+ model_call.content = generate_content()
24
+ end
25
+
26
+ def self.generate_content(pos=0, page_num=1)
27
+ i = 0
28
+ @current_page = page_num
29
+ start_pos = (page_num - 1) * 9
30
+ end_pos = start_pos + 9
31
+ call_list_str = ""
32
+ @last_pos = 0
33
+ @call_list.each do |call|
34
+ i += 1
35
+ model = ORM::Model[call.model_id]
36
+ row_str = "id: #{call.call_id}"
37
+ row_str = row_str + " "*(10-row_str.length) + "model:" + model.model_provider + "/" + model.model_name
38
+ if call.prompt_list.length < 42
39
+ row_str = row_str + " "*(60-row_str.length) + "prompts: "+ call.prompt_list
40
+ else
41
+ row_str = row_str + " "*(60-row_str.length) + "prompts: ..."+ call.prompt_list[-42..-1]
42
+ end
43
+ if i > start_pos && i <= end_pos
44
+ if i-start_pos==pos
45
+ call_list_str += RubyRich::AnsiCode.color(:blue)+ "#{i-start_pos} #{row_str}\n" + RubyRich::AnsiCode.reset
46
+ else
47
+ call_list_str += RubyRich::AnsiCode.color(:blue)+ "#{i-start_pos}" + RubyRich::AnsiCode.reset + " #{row_str}\n"
48
+ end
49
+ @last_pos += 1
50
+ end
51
+ end
52
+ @max_pos = i
53
+ if start_pos>0 or i > end_pos
54
+ call_list_str += " \n" * (11 - call_list_str.split("\n").size)
55
+ end
56
+ if start_pos>0
57
+ call_list_str += " " * 11 + "←" + " " * 2
58
+ else
59
+ call_list_str += " " * 14
60
+ end
61
+ if i > end_pos
62
+ call_list_str += " " * 2 + "→"
63
+ end
64
+ return call_list_str
65
+ end
66
+
67
+ def self.try_parse(string)
68
+ return JSON.parse(string)
69
+ rescue JSON::ParserError
70
+ begin
71
+ return JSON.parse(string.gsub("=>",":").gsub("nil","\"nil\""))
72
+ rescue JSON::ParserError
73
+ return false
74
+ end
75
+ end
76
+
77
+ def self.show_call(model_call)
78
+ model_call.content = generate_content(@current_pos, @current_page)
79
+ call = @call_list[(@current_page - 1)*9+@current_pos-1]
80
+ response = ORM::Response.where(call_id: call.call_id).first
81
+ if response
82
+ response_content = response.response_content
83
+ if json = try_parse(response_content)
84
+ Response.set_content(json.dig("choices", 0, "message", "content"))
85
+ Response.set_reason_content(json.dig("choices", 0, "message", "reasoning_content"))
86
+ Response.set_tool_content(json.dig("choices",0,"message", "tool_calls"))
87
+ else
88
+ Response.set_content(response_content)
89
+ Response.set_reason_content("")
90
+ Response.set_tool_content("")
91
+ end
92
+ response_id = response.response_id
93
+ feedback = ORM::Feedback.where(response_id: response_id).first
94
+ else
95
+ Response.set_content("响应不存在")
96
+ Response.set_reason_content("响应不存在")
97
+ Response.set_tool_content("响应不存在")
98
+ end
99
+ Response.show_content(:content, feedback)
100
+ return feedback, response_id
101
+ end
102
+
103
+ def self.register_event_listener(layout)
104
+ layout.key(:string) do |event, live|
105
+ model_call = live.find_panel("model_call")
106
+ if model_call.border_style == :green && @call_list
107
+ if event[:value].to_i >= 1 && event[:value].to_i <= @last_pos
108
+ @current_pos = event[:value].to_i
109
+ live.params[:feedback], live.params[:response_id] = show_call(model_call)
110
+ end
111
+ end
112
+ end
113
+ layout.key(:right) do |event, live|
114
+ model_call = live.find_panel("model_call")
115
+ if model_call.border_style == :green && @call_list
116
+ if @current_page<@page_count
117
+ model_call.content = generate_content(0, @current_page+1)
118
+ @current_pos = 0
119
+ end
120
+ end
121
+ end
122
+ layout.key(:left) do |event, live|
123
+ model_call = live.find_panel("model_call")
124
+ if model_call.border_style == :green && @call_list
125
+ if @current_page>1
126
+ model_call.content = generate_content(0, @current_page-1)
127
+ @current_pos = 0
128
+ end
129
+ end
130
+ end
131
+ layout.key(:up) do |event, live|
132
+ model_call = live.find_panel("model_call")
133
+ if model_call.border_style == :green && @call_list
134
+ if @current_pos > 1
135
+ @current_pos -= 1
136
+ show_call(model_call)
137
+ end
138
+ end
139
+ end
140
+ layout.key(:down) do |event, live|
141
+ model_call = live.find_panel("model_call")
142
+ if model_call.border_style == :green && @call_list
143
+ if @current_pos < @max_pos
144
+ @current_pos += 1
145
+ show_call(model_call)
146
+ end
147
+ end
148
+ end
149
+ layout.key(:ctrl_f) do |event, live|
150
+ model_call = live.find_panel("model_call")
151
+ if model_call.border_style == :green && @current_pos>0 && @call_list
152
+ dialog = Feedback.build(live)
153
+ live.layout.show_dialog(dialog)
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,112 @@
1
+ module BetterPrompt
2
+ module Component
3
+ class PromptList
4
+ def self.build
5
+ layout = RubyRich::Layout.new(name: "prompt_list", ratio: 4)
6
+ draw_view(layout)
7
+ register_event_listener(layout)
8
+ return layout
9
+ end
10
+
11
+ def self.draw_view(layout)
12
+ layout.update_content(RubyRich::Panel.new(
13
+ "",
14
+ title: "提示词列表 (Shift+2)"
15
+ ))
16
+ end
17
+
18
+ def self.update_title(prompt)
19
+ result = BetterPrompt.engine.call_worker(:summarize, {text: prompt[:prompt_content]})
20
+ prompt[:prompt_title] = result.strip
21
+ prompt.save
22
+ return result.strip
23
+ end
24
+
25
+ def self.update_prompt_list(live, template_name)
26
+ prompt_list = live.find_panel("prompt_list")
27
+ @prompt_list = BetterPrompt::ORM::Prompt.where(:prompt_template_name=>template_name).all
28
+ @page_count = (@prompt_list.size / 9.0).ceil
29
+ prompt_list.content = generate_content()
30
+ end
31
+
32
+ def self.generate_content(pos=0, page_num=1)
33
+ i = 0
34
+ @current_page = page_num
35
+ start_pos = (page_num - 1) * 9
36
+ end_pos = start_pos + 9
37
+ prompt_list_str = ""
38
+ @prompt_list.each do |prompt|
39
+ i += 1
40
+ title = prompt.prompt_title
41
+ if title==nil
42
+ title = update_title(prompt)
43
+ end
44
+ title_width = title.chars.sum { |c| Unicode::DisplayWidth.of(c) }
45
+ if title_width>22
46
+ new_title = ""
47
+ title.chars.each do |c|
48
+ if (new_title+c).chars.sum { |c| Unicode::DisplayWidth.of(c) }>22
49
+ new_title += ".."
50
+ break
51
+ else
52
+ new_title += c
53
+ end
54
+ end
55
+ title = new_title
56
+ end
57
+ if i > start_pos && i <= end_pos
58
+ if i-start_pos==pos
59
+ prompt_list_str += RubyRich::AnsiCode.color(:blue)+ "#{i-start_pos} #{title}\n" + RubyRich::AnsiCode.reset
60
+ else
61
+ prompt_list_str += RubyRich::AnsiCode.color(:blue)+ "#{i-start_pos}" + RubyRich::AnsiCode.reset + " #{title}\n"
62
+ end
63
+ end
64
+ end
65
+ if start_pos>0 or i > end_pos
66
+ prompt_list_str += " \n" * (25 - prompt_list_str.split("\n").size)
67
+ end
68
+ if start_pos>0
69
+ prompt_list_str += " " * 11 + "←" + " " * 2
70
+ else
71
+ prompt_list_str += " " * 14
72
+ end
73
+ if i > end_pos
74
+ prompt_list_str += " " * 2 + "→"
75
+ end
76
+ return prompt_list_str
77
+ end
78
+
79
+ def self.register_event_listener(layout)
80
+ layout.key(:string) do |event, live|
81
+ prompt_list = live.find_panel("prompt_list")
82
+ if prompt_list.border_style == :green
83
+ if event[:value].to_i >= 1 && event[:value].to_i <= @prompt_list.count
84
+ prompt_list.content = generate_content(event[:value].to_i, @current_page)
85
+ prompt = @prompt_list[(@current_page - 1)*9+event[:value].to_i-1]
86
+ dialog = ShowPrompt.build(prompt.prompt_content, live)
87
+ live.layout.show_dialog(dialog)
88
+ Response.clean
89
+ ModelCall.update_model_call_list(live, prompt.prompt_id)
90
+ end
91
+ end
92
+ end
93
+ layout.key(:right) do |event, live|
94
+ prompt_list = live.find_panel("prompt_list")
95
+ if prompt_list.border_style == :green
96
+ if @current_page<@page_count
97
+ prompt_list.content = generate_content(0, @current_page+1)
98
+ end
99
+ end
100
+ end
101
+ layout.key(:left) do |event, live|
102
+ prompt_list = live.find_panel("prompt_list")
103
+ if prompt_list.border_style == :green
104
+ if @current_page>1
105
+ prompt_list.content = generate_content(0, @current_page-1)
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,74 @@
1
+ module BetterPrompt
2
+ module Component
3
+ class Response
4
+ def self.build
5
+ layout = RubyRich::Layout.new(name: "response", ratio: 1)
6
+ draw_view(layout)
7
+ register_event_listener(layout)
8
+ return layout
9
+ end
10
+
11
+ def self.draw_view(layout)
12
+ @panel = RubyRich::Panel.new(
13
+ "",
14
+ title: "回答 (Ctrl+r/Ctrl+a/Ctrl+t) 评分: 1~5"
15
+ )
16
+ layout.update_content(@panel)
17
+ end
18
+
19
+ def self.clean
20
+ @panel.content = ""
21
+ end
22
+
23
+ def self.update_content(content)
24
+ if content.strip.empty?
25
+ @panel.content = "<Empty>"
26
+ else
27
+ @panel.content = ""
28
+ @panel.content = content
29
+ end
30
+ @panel.home
31
+ end
32
+
33
+ def self.set_content(content)
34
+ @content = content
35
+ end
36
+
37
+ def self.set_reason_content(content)
38
+ @reason_content = content
39
+ end
40
+
41
+ def self.set_tool_content(content)
42
+ @tool_content = content
43
+ end
44
+
45
+ def self.show_content(type, feedback)
46
+ rating = if feedback
47
+ feedback.rating
48
+ else
49
+ "1~5"
50
+ end
51
+ if type==:content
52
+ @panel.title = "回答 (Ctrl+r/Ctrl+a/Ctrl+t) 评分: #{rating}"
53
+ update_content(@content.to_s)
54
+ end
55
+ if type==:reason
56
+ @panel.title = "推理 (Ctrl+r/Ctrl+a/Ctrl+t) 评分: #{rating}"
57
+ update_content(@reason_content.to_s)
58
+ end
59
+ if type==:tool
60
+ @panel.title = "工具调用 (Ctrl+r/Ctrl+a/Ctrl+t) 评分: #{rating}"
61
+ update_content(@tool_content.to_s)
62
+ end
63
+ end
64
+
65
+ def self.register_event_listener(layout)
66
+ layout.key(:ctrl_r) { |event, live| show_content(:reason, live.params[:feedback])}
67
+ layout.key(:ctrl_a) { |event, live| show_content(:content, live.params[:feedback])}
68
+ layout.key(:ctrl_t) { |event, live| show_content(:tool, live.params[:feedback])}
69
+ layout.key(:page_up) { |event, live| @panel.page_up}
70
+ layout.key(:page_down) { |event, live| @panel.page_down}
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,38 @@
1
+ module BetterPrompt
2
+ module Component
3
+ class Root
4
+ def self.build
5
+ layout = RubyRich::Layout.new
6
+ register_event_listener(layout)
7
+ return layout
8
+ end
9
+ def self.register_event_listener(layout)
10
+ layout.key(:string) do |event, live|
11
+ key = event[:value]
12
+ if key=="!"
13
+ template_list = live.find_panel("template_list")
14
+ template_list.border_style = :green
15
+ prompt_list = live.find_panel("prompt_list")
16
+ prompt_list.border_style = :white
17
+ model_call = live.find_panel("model_call")
18
+ model_call.border_style = :white
19
+ elsif key=="@"
20
+ template_list = live.find_panel("template_list")
21
+ template_list.border_style = :white
22
+ prompt_list = live.find_panel("prompt_list")
23
+ prompt_list.border_style = :green
24
+ model_call = live.find_panel("model_call")
25
+ model_call.border_style = :white
26
+ elsif key=="#"
27
+ template_list = live.find_panel("template_list")
28
+ template_list.border_style = :white
29
+ prompt_list = live.find_panel("prompt_list")
30
+ prompt_list.border_style = :white
31
+ model_call = live.find_panel("model_call")
32
+ model_call.border_style = :green
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,19 @@
1
+ module BetterPrompt
2
+ module Component
3
+ class ShowPrompt
4
+ def self.build(content, live)
5
+ width = 60
6
+ dialog = RubyRich::Dialog.new(title: "提示词内容", content: content, width: width, height: 20, buttons: [:ok])
7
+ dialog.live = live
8
+ register_event_listener(dialog)
9
+ return dialog
10
+ end
11
+
12
+ def self.register_event_listener(dialog)
13
+ dialog.key(:enter){|event, live|
14
+ live.layout.hide_dialog
15
+ }
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,15 @@
1
+ module BetterPrompt
2
+ module Component
3
+ class Sidebar
4
+ def self.build
5
+ layout = RubyRich::Layout.new(name: "sidebar", size: 30)
6
+ register_event_listener(layout)
7
+ return layout
8
+ end
9
+
10
+ def self.register_event_listener(layout)
11
+ layout.key(:ctrl_c) { |event, live| live.stop }
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,90 @@
1
+ module BetterPrompt
2
+ module Component
3
+ class TemplateList
4
+ def self.build
5
+ layout = RubyRich::Layout.new(name: "template_list", size: 14)
6
+ draw_view(layout)
7
+ register_event_listener(layout)
8
+ return layout
9
+ end
10
+
11
+ def self.get_list_content
12
+ @template_list = ORM::Prompt
13
+ .where(prompt_template_name: 'NULL')
14
+ .invert
15
+ .select(:prompt_template_name)
16
+ .distinct
17
+ .map(&:prompt_template_name)
18
+ @page_count = (@template_list.size / 9.0).ceil
19
+ generate_content
20
+ end
21
+
22
+ def self.generate_content(pos=0, page_num=1)
23
+ i = 0
24
+ @current_page = page_num
25
+ start_pos = (page_num - 1) * 9
26
+ end_pos = start_pos + 9
27
+ template_list_str = ""
28
+ @template_list.each do |template|
29
+ i += 1
30
+ if i > start_pos && i <= end_pos
31
+ if i-start_pos==pos
32
+ template_list_str += RubyRich::AnsiCode.color(:blue)+ "#{i-start_pos} #{template}\n" + RubyRich::AnsiCode.reset
33
+ else
34
+ template_list_str += RubyRich::AnsiCode.color(:blue)+ "#{i-start_pos}" + RubyRich::AnsiCode.reset + " #{template}\n"
35
+ end
36
+ end
37
+ end
38
+ if start_pos>0 or i > end_pos
39
+ template_list_str += " \n" * (11 - template_list_str.split("\n").size)
40
+ end
41
+ if start_pos>0
42
+ template_list_str += " " * 11 + "←" + " " * 2
43
+ else
44
+ template_list_str += " " * 14
45
+ end
46
+ if i > end_pos
47
+ template_list_str += " " * 2 + "→"
48
+ end
49
+ return template_list_str
50
+ end
51
+
52
+ def self.draw_view(layout)
53
+ layout.update_content(RubyRich::Panel.new(
54
+ get_list_content(),
55
+ title: "模板列表 (Shift+1)",
56
+ border_style: :green
57
+ ))
58
+ end
59
+
60
+ def self.register_event_listener(layout)
61
+ layout.key(:string) do |event, live|
62
+ template_list = live.find_panel("template_list")
63
+ if template_list.border_style == :green
64
+ if event[:value].to_i >= 1 && event[:value].to_i <= @template_list.count
65
+ template_list.content = generate_content(event[:value].to_i, @current_page)
66
+ template_name = @template_list[(@current_page - 1)*9+event[:value].to_i-1]
67
+ PromptList.update_prompt_list(live, template_name)
68
+ end
69
+ end
70
+ end
71
+ layout.key(:right) do |event, live|
72
+ template_list = live.find_panel("template_list")
73
+ if template_list.border_style == :green
74
+ if @current_page<@page_count
75
+ template_list.content = generate_content(0, @current_page+1)
76
+ end
77
+ end
78
+ end
79
+ layout.key(:left) do |event, live|
80
+ template_list = live.find_panel("template_list")
81
+ if template_list.border_style == :green
82
+ if @current_page>1
83
+ template_list.content = generate_content(0, @current_page-1)
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,31 @@
1
+ require 'sequel'
2
+ module BetterPrompt
3
+ module ORM
4
+ class Model < Sequel::Model(ORM.db[:models])
5
+ plugin :timestamps, update_on_create: true
6
+ end
7
+
8
+ class Prompt < Sequel::Model(ORM.db[:prompts])
9
+ plugin :timestamps, update_on_create: true
10
+ end
11
+
12
+ class ModelCall < Sequel::Model(ORM.db[:model_calls])
13
+ plugin :timestamps, update_on_create: true
14
+ end
15
+
16
+ class Response < Sequel::Model(ORM.db[:responses])
17
+ plugin :timestamps, update_on_create: true
18
+ end
19
+
20
+ class Feedback < Sequel::Model(ORM.db[:feedback])
21
+ plugin :timestamps, update_on_create: true
22
+ end
23
+
24
+ class Tag < Sequel::Model(ORM.db[:tags])
25
+ plugin :timestamps, update_on_create: true
26
+ end
27
+
28
+ class PromptTag < Sequel::Model(ORM.db[:prompt_tags])
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,16 @@
1
+ require 'sequel'
2
+ module BetterPrompt
3
+ module ORM
4
+ class << self
5
+ attr_reader :db
6
+
7
+ def setup(database_url)
8
+ raise "DATABASE_URL environment variable not set" unless database_url
9
+ @db = Sequel.connect(database_url)
10
+ @db.test_connection
11
+ rescue => e
12
+ raise "Failed to connect to database: #{e.message}"
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,3 @@
1
+ module BetterPrompt
2
+ VERSION = "0.2.0"
3
+ end
@@ -0,0 +1,158 @@
1
+ require "sqlite3"
2
+ require "fileutils"
3
+ require "time"
4
+ require "smart_prompt"
5
+
6
+ require_relative 'better_prompt/cli'
7
+ require_relative 'better_prompt/orm'
8
+ Dir["#{__dir__}/better_prompt/components/*.rb"].each { |f| require_relative f }
9
+
10
+ module BetterPrompt
11
+ class << self
12
+ def setup(db_path:)
13
+ BetterPrompt.logger = Logger.new("./log/better.log")
14
+ @db_path = db_path
15
+ # Return true if database already exists
16
+ if File.exist?(db_path)
17
+ ORM.setup("sqlite://"+db_path)
18
+ require_relative 'better_prompt/models'
19
+ return true
20
+ end
21
+
22
+ # Ensure directory exists
23
+ FileUtils.mkdir_p(File.dirname(db_path))
24
+
25
+ # Initialize database
26
+ SQLite3::Database.new(db_path) do |db|
27
+ # Execute each statement from init.sql
28
+ File.read(File.join(__dir__, "../db/init.sql")).split(/;\s*\n/).each do |statement|
29
+ db.execute(statement.strip) unless statement.strip.empty?
30
+ end
31
+ end
32
+ ORM.setup("sqlite://"+db_path)
33
+ require_relative 'better_prompt/models'
34
+ true
35
+ rescue => e
36
+ raise "Failed to initialize database: #{e.message}"
37
+ end
38
+
39
+ def add_model(provider, model_name)
40
+ model_version = if model_name.include?(':')
41
+ model_name.split(':', 2).last
42
+ else
43
+ 'latest'
44
+ end
45
+ model_name = model_name.split(":")[0]
46
+
47
+ ORM::Model.find_or_create(
48
+ model_provider: provider,
49
+ model_name: model_name,
50
+ model_version: model_version
51
+ )
52
+ rescue => e
53
+ raise "Failed to add model: #{e.message}"
54
+ end
55
+
56
+ def add_prompt(template_name, role, prompt_content)
57
+ prompt_length = prompt_content.length
58
+
59
+ ORM::Prompt.find_or_create(
60
+ prompt_template_name: template_name,
61
+ role: role,
62
+ prompt_content: prompt_content
63
+ ) do |prompt|
64
+ prompt.prompt_length = prompt_length
65
+ end
66
+ rescue => e
67
+ raise "Failed to add prompt: #{e.message}"
68
+ end
69
+
70
+ def add_model_call(provider, model_name, messages, streaming=false, temperature=0.7, max_tokens=0, top_p=0.0, top_k=0, params={})
71
+ model_version = if model_name.include?(':')
72
+ model_name.split(':', 2).last
73
+ else
74
+ 'latest'
75
+ end
76
+ model_base_name = model_name.split(":")[0]
77
+
78
+ # 查找或创建model记录
79
+ model = ORM::Model.first(
80
+ model_provider: provider,
81
+ model_name: model_base_name,
82
+ model_version: model_version
83
+ )
84
+ raise "Model not found: #{provider}/#{model_name}" unless model
85
+
86
+ # 从messages构建prompt_list
87
+ prompt_list = messages.map do |msg|
88
+ role = msg[:role] || msg["role"]
89
+ content = msg[:content] || msg["content"]
90
+ if role == "assistant" || role == "tool"
91
+ add_prompt("NULL", role, content)
92
+ end
93
+ # 查找prompt_id
94
+ prompt = ORM::Prompt.first(role: role, prompt_content: content)
95
+ prompt&.prompt_id || 0
96
+ end.to_json
97
+
98
+ # 创建model_call记录
99
+ model_call = ORM::ModelCall.create(
100
+ prompt_list: prompt_list,
101
+ model_id: model.model_id,
102
+ is_streaming: streaming,
103
+ temperature: temperature,
104
+ max_tokens: max_tokens,
105
+ top_p: top_p,
106
+ top_k: top_k,
107
+ additional_parameters: params.to_json
108
+ )
109
+
110
+ model_call.call_id
111
+ rescue => e
112
+ raise "Failed to add model call: #{e.message}"
113
+ end
114
+
115
+ def add_response(call_id, response_content, is_streaming)
116
+ response_content = response_content.to_s
117
+ response_length = response_content.length
118
+ current_time = Time.now
119
+
120
+ # 查找model_call记录
121
+ model_call = ORM::ModelCall[call_id]
122
+ raise "Model call not found: #{call_id}" unless model_call
123
+
124
+ # 计算响应时间(毫秒)
125
+ response_time_ms = ((current_time - model_call.call_timestamp) * 1000).to_i
126
+
127
+ # 创建response记录
128
+ response = ORM::Response.create(
129
+ call_id: call_id,
130
+ response_content: response_content,
131
+ response_length: response_length,
132
+ response_time_ms: response_time_ms,
133
+ is_streaming: is_streaming
134
+ )
135
+
136
+ response.response_id
137
+ rescue => e
138
+ raise "Failed to add response: #{e.message}"
139
+ end
140
+ def engine
141
+ if @engine == nil
142
+ file_path = "./config/config.yml"
143
+ file_path = "./config/llm_config.yml" unless File.exist?(file_path)
144
+ BetterPrompt.logger.info(file_path)
145
+ @engine = SmartPrompt::Engine.new(file_path)
146
+ end
147
+ return @engine
148
+ end
149
+ def logger=(logger)
150
+ @logger = logger
151
+ end
152
+ def logger
153
+ @logger ||= Logger.new($stdout).tap do |log|
154
+ log.progname = "Better Prompt"
155
+ end
156
+ end
157
+ end
158
+ end
metadata ADDED
@@ -0,0 +1,130 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: better_prompt
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - zhuang biaowei
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: ruby_rich
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: 0.3.1
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: 0.3.1
26
+ - !ruby/object:Gem::Dependency
27
+ name: sqlite3
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.4'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.4'
40
+ - !ruby/object:Gem::Dependency
41
+ name: bundler
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '2.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '2.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rake
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '13.0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '13.0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: rspec
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '3.0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '3.0'
82
+ description: Provides enhanced command line prompt functionality with database storage
83
+ email:
84
+ - zbw@kaiyuanshe.org
85
+ executables:
86
+ - bp
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - LICENSE
91
+ - README.cn.md
92
+ - README.md
93
+ - bin/bp
94
+ - db/init.sql
95
+ - lib/better_prompt.rb
96
+ - lib/better_prompt/cli.rb
97
+ - lib/better_prompt/components/feedback.rb
98
+ - lib/better_prompt/components/main.rb
99
+ - lib/better_prompt/components/model_call.rb
100
+ - lib/better_prompt/components/prompt_list.rb
101
+ - lib/better_prompt/components/response.rb
102
+ - lib/better_prompt/components/root.rb
103
+ - lib/better_prompt/components/show_prompt.rb
104
+ - lib/better_prompt/components/sidebar.rb
105
+ - lib/better_prompt/components/template_list.rb
106
+ - lib/better_prompt/models.rb
107
+ - lib/better_prompt/orm.rb
108
+ - lib/better_prompt/version.rb
109
+ homepage: https://github.com/zhuangbiaowei/better_prompt
110
+ licenses:
111
+ - MIT
112
+ metadata: {}
113
+ rdoc_options: []
114
+ require_paths:
115
+ - lib
116
+ required_ruby_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: 2.6.0
121
+ required_rubygems_version: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ requirements: []
127
+ rubygems_version: 3.6.9
128
+ specification_version: 4
129
+ summary: A better command line prompt utility
130
+ test_files: []