fewald-worklog 0.3.19 → 0.3.20
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 +4 -4
- data/.version +1 -1
- data/lib/cli.rb +19 -0
- data/lib/log_entry.rb +0 -20
- data/lib/log_entry_formatters.rb +20 -0
- data/lib/standup.rb +100 -0
- data/lib/worklog.rb +9 -3
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5ca2ba5c3d5bc431914b344733083fd6932fbac57f24a03e13353234cc7695c6
|
|
4
|
+
data.tar.gz: 57046a8bc5b57f68102dec97320536617cf5c0fbdc5042480ca1cdb0b3c2614b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 755810f1c1a1fbdae57f3ed6c0b9b16cd1af7c51a7be61a80280087583788aa222e92d27691366f635bd5eea7b0d1ac3583696929a6721486808995b9ef53488
|
|
7
|
+
data.tar.gz: d303ea6a3ab17ffb09317de1b6a15ca378b7dd07722e96ed4fcdc26486e608e499db0e0203845ddc757e04b4a28068bfda4e0e03998d67c208e83affafb38248
|
data/.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.3.
|
|
1
|
+
0.3.20
|
data/lib/cli.rb
CHANGED
|
@@ -134,6 +134,25 @@ class WorklogCLI < Thor
|
|
|
134
134
|
end
|
|
135
135
|
end
|
|
136
136
|
|
|
137
|
+
desc 'standup', 'Show the standup report for the current day'
|
|
138
|
+
option :date, type: :string, default: Date.today.to_s,
|
|
139
|
+
desc: <<~DESC
|
|
140
|
+
Show the standup report for a specific date. If this option is provided, --from and --to and --days should not be used.
|
|
141
|
+
DESC
|
|
142
|
+
option :from, type: :string, desc: <<~EOF
|
|
143
|
+
Inclusive start date of the range. Takes precedence over --date, if defined.
|
|
144
|
+
EOF
|
|
145
|
+
option :to, type: :string, desc: <<~EOF
|
|
146
|
+
Inclusive end date of the range. Takes precedence over --date, if defined.
|
|
147
|
+
EOF
|
|
148
|
+
option :days, type: :numeric, desc: <<~EOF
|
|
149
|
+
Number of days to show starting from --date. Takes precedence over --from and --to if defined.
|
|
150
|
+
EOF
|
|
151
|
+
def standup
|
|
152
|
+
worklog = Worklog::Worklog.new
|
|
153
|
+
worklog.standup(options)
|
|
154
|
+
end
|
|
155
|
+
|
|
137
156
|
desc 'tags', 'Show all tags used in the work log'
|
|
138
157
|
option :date, type: :string, default: Date.today.to_s,
|
|
139
158
|
desc: <<~DESC
|
data/lib/log_entry.rb
CHANGED
|
@@ -108,25 +108,5 @@ module Worklog
|
|
|
108
108
|
time == other.time && tags == other.tags && ticket == other.ticket && url == other.url &&
|
|
109
109
|
epic == other.epic && message == other.message
|
|
110
110
|
end
|
|
111
|
-
|
|
112
|
-
private
|
|
113
|
-
|
|
114
|
-
# Prefix for epic entries with formatting.
|
|
115
|
-
# @return [String]
|
|
116
|
-
def epic_prefix
|
|
117
|
-
"#{Rainbow('[EPIC]').bg(:white).fg(:black)} "
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
# Format metadata for display.
|
|
121
|
-
# @return [String]
|
|
122
|
-
def format_metadata
|
|
123
|
-
metadata_parts = []
|
|
124
|
-
metadata_parts << Rainbow(@ticket).fg(:blue) if @ticket
|
|
125
|
-
metadata_parts << @tags.join(', ') if @tags&.any?
|
|
126
|
-
metadata_parts << @url if @url && @url != ''
|
|
127
|
-
metadata_parts << @project if @project && @project != ''
|
|
128
|
-
|
|
129
|
-
metadata_parts.empty? ? '' : " [#{metadata_parts.join('] [')}]"
|
|
130
|
-
end
|
|
131
111
|
end
|
|
132
112
|
end
|
data/lib/log_entry_formatters.rb
CHANGED
|
@@ -85,6 +85,26 @@ module Worklog
|
|
|
85
85
|
def format(log_entry)
|
|
86
86
|
replace_people_handles(log_entry, log_entry.message.dup)
|
|
87
87
|
end
|
|
88
|
+
|
|
89
|
+
protected
|
|
90
|
+
|
|
91
|
+
# Replace people handles in the message with their names.
|
|
92
|
+
# @param known_people Hash[String, Person] A hash of people with their handles as keys.
|
|
93
|
+
# @param msg [String] the message to replace handles in.
|
|
94
|
+
# @return [String] the message with replaced handles.
|
|
95
|
+
def replace_people_handles(log_entry, msg)
|
|
96
|
+
log_entry.people.each do |person|
|
|
97
|
+
next unless @known_people && @known_people[person]
|
|
98
|
+
|
|
99
|
+
msg.gsub!(/[~@]#{person}/) do |match|
|
|
100
|
+
s = String.new
|
|
101
|
+
s << ' ' if match[0] == ' '
|
|
102
|
+
s << @known_people[person].name if @known_people && @known_people[person]
|
|
103
|
+
s
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
msg
|
|
107
|
+
end
|
|
88
108
|
end
|
|
89
109
|
end
|
|
90
110
|
end
|
data/lib/standup.rb
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'dotenv-vault/load'
|
|
4
|
+
require 'erb'
|
|
5
|
+
require 'faraday'
|
|
6
|
+
require 'langchain'
|
|
7
|
+
require 'openai'
|
|
8
|
+
require 'worklogger'
|
|
9
|
+
require 'log_entry_formatters'
|
|
10
|
+
|
|
11
|
+
module Worklog
|
|
12
|
+
# Generates standup prompts based on log entries.
|
|
13
|
+
# The Standup class takes log entries and people information to create a standup message that summarizes the work
|
|
14
|
+
# done, work planned, and blockers.
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# # Create log entries (or alternatively load from disk)
|
|
18
|
+
# entries = [LogEntry.new(...), LogEntry.new(...)]
|
|
19
|
+
#
|
|
20
|
+
# # Create people information (or alternatively load from disk)
|
|
21
|
+
# people = { 'user1' => Person.new(...), 'user2' => Person.new(...) }
|
|
22
|
+
#
|
|
23
|
+
# # Create a Standup instance and generate the standup message
|
|
24
|
+
# standup = Standup.new(entries, people)
|
|
25
|
+
# standup.generate
|
|
26
|
+
class Standup
|
|
27
|
+
# Initialize the Standup generator with log entries and people information.
|
|
28
|
+
# If no people information is provided, identifiers will not be resolved to names.
|
|
29
|
+
# @param entries [Array<LogEntry>] the log entries to include in the standup.
|
|
30
|
+
# These entries can be either of a single day or span multiple days, depending on the desired output.
|
|
31
|
+
# They are handled by the LLM to generate a standup message that summarizes the work done, work planned,
|
|
32
|
+
# and blockers.
|
|
33
|
+
# @param people [Hash<String, Person>] the people information to include in the standup
|
|
34
|
+
def initialize(entries, people)
|
|
35
|
+
@entries = entries
|
|
36
|
+
@people = people
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Generate the standup message and print it to the console.
|
|
40
|
+
# @return [nil] prints the generated standup message to the console.
|
|
41
|
+
def generate
|
|
42
|
+
Langchain.logger.level = Logger::WARN
|
|
43
|
+
|
|
44
|
+
system_prompt, user_prompt = create_prompt
|
|
45
|
+
|
|
46
|
+
llm = Langchain::LLM::OpenAI.new(api_key: ENV.fetch('OPENAI_API_KEY', nil),
|
|
47
|
+
default_options: {
|
|
48
|
+
chat_model: 'gpt-5.2' # or any other supported model
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
# Override the client to increase timeout
|
|
52
|
+
def llm.client
|
|
53
|
+
@client ||= Faraday.new(url: url, headers: auth_headers) do |conn|
|
|
54
|
+
conn.options.timeout = 300
|
|
55
|
+
conn.request :json
|
|
56
|
+
conn.response :json
|
|
57
|
+
conn.response :raise_error
|
|
58
|
+
conn.response :logger, Langchain.logger, { headers: true, bodies: true, errors: true }
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
assistant = Langchain::Assistant.new(
|
|
63
|
+
llm: llm,
|
|
64
|
+
instructions: system_prompt
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
assistant.add_message(role: 'user', content: user_prompt)
|
|
68
|
+
|
|
69
|
+
WorkLogger.debug('Starting standup generation')
|
|
70
|
+
begin
|
|
71
|
+
assistant.run(auto_tool_execution: true)
|
|
72
|
+
rescue Faraday::ForbiddenError => e
|
|
73
|
+
WorkLogger.error("LLM request failed: #{e.response}")
|
|
74
|
+
raise
|
|
75
|
+
end
|
|
76
|
+
WorkLogger.debug('Finished standup generation')
|
|
77
|
+
puts Rainbow('Standup generated successfully!').yellow
|
|
78
|
+
puts assistant.messages.last.content
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Create the system and user prompts for standup generation.
|
|
82
|
+
# @return [Array<String>] the system prompt and user prompt.
|
|
83
|
+
def create_prompt
|
|
84
|
+
system_prompt_template = File.read(File.join(__dir__, '..', 'assets', 'prompts', 'standup.system.md.erb'))
|
|
85
|
+
system_prompt = ERB.new(system_prompt_template, trim_mode: '-<>').result(binding)
|
|
86
|
+
|
|
87
|
+
user_prompt_template = File.read(File.join(__dir__, '..', 'assets', 'prompts', 'standup.user.md.erb'))
|
|
88
|
+
user_prompt = ERB.new(user_prompt_template, trim_mode: '-<>').result(binding)
|
|
89
|
+
|
|
90
|
+
[system_prompt, user_prompt]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def formatted_entries
|
|
94
|
+
formatter = LogEntryFormatters::SimpleFormatter.new(@people)
|
|
95
|
+
@entries.map do |entry|
|
|
96
|
+
entry.to_hash.slice(:epic, :ticket, :url).merge(message: formatter.format(entry))
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
data/lib/worklog.rb
CHANGED
|
@@ -16,6 +16,7 @@ require 'string_helper'
|
|
|
16
16
|
require 'people_storage'
|
|
17
17
|
require 'printer'
|
|
18
18
|
require 'project_storage'
|
|
19
|
+
require 'standup'
|
|
19
20
|
require 'statistics'
|
|
20
21
|
require 'storage'
|
|
21
22
|
require 'summary'
|
|
@@ -153,7 +154,6 @@ module Worklog
|
|
|
153
154
|
|
|
154
155
|
events_by_date.each do |date, events|
|
|
155
156
|
WorkLogger.info Rainbow("Found #{events.size} events for #{date}").green
|
|
156
|
-
puts "Processing #{events.size} events for '#{date}'"
|
|
157
157
|
@storage.create_file_skeleton(date)
|
|
158
158
|
daily_log = @storage.load_log!(@storage.filepath(date))
|
|
159
159
|
dirty = false
|
|
@@ -163,7 +163,7 @@ module Worklog
|
|
|
163
163
|
|
|
164
164
|
# Check if entry already exists
|
|
165
165
|
if daily_log.key?(log_entry.key)
|
|
166
|
-
WorkLogger.debug(
|
|
166
|
+
WorkLogger.debug("Entry with key #{log_entry.key} already exists in #{log_entry.time.to_date}, skipping")
|
|
167
167
|
else
|
|
168
168
|
daily_log << log_entry
|
|
169
169
|
# Mark log as dirty to trigger write later
|
|
@@ -176,7 +176,7 @@ module Worklog
|
|
|
176
176
|
if dirty
|
|
177
177
|
@storage.write_log(@storage.filepath(date), daily_log)
|
|
178
178
|
else
|
|
179
|
-
WorkLogger.info
|
|
179
|
+
WorkLogger.info "No new entries to add for #{date}, skipping write."
|
|
180
180
|
end
|
|
181
181
|
end
|
|
182
182
|
end
|
|
@@ -333,6 +333,12 @@ module Worklog
|
|
|
333
333
|
puts 'No projects found.' if projects.empty?
|
|
334
334
|
end
|
|
335
335
|
|
|
336
|
+
def standup(options = {})
|
|
337
|
+
start_date, end_date = start_end_date(options)
|
|
338
|
+
entries = @storage.days_between(start_date, end_date).map(&:entries).flatten
|
|
339
|
+
Standup.new(entries, @people).generate
|
|
340
|
+
end
|
|
341
|
+
|
|
336
342
|
# Show all tags used in the work log or details for a specific tag
|
|
337
343
|
#
|
|
338
344
|
# @param tag [String, nil] the tag to show details for, or nil to show all tags
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fewald-worklog
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.20
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Friedrich Ewald
|
|
@@ -156,6 +156,7 @@ files:
|
|
|
156
156
|
- lib/printer.rb
|
|
157
157
|
- lib/project.rb
|
|
158
158
|
- lib/project_storage.rb
|
|
159
|
+
- lib/standup.rb
|
|
159
160
|
- lib/statistics.rb
|
|
160
161
|
- lib/storage.rb
|
|
161
162
|
- lib/string_helper.rb
|