circuit_breaker-wf 0.1.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 +7 -0
- data/.gitignore +4 -0
- data/CHANGELOG.md +52 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +116 -0
- data/LICENSE +21 -0
- data/README.md +324 -0
- data/examples/document/README.md +150 -0
- data/examples/document/document_assistant.rb +535 -0
- data/examples/document/document_rules.rb +60 -0
- data/examples/document/document_token.rb +83 -0
- data/examples/document/document_workflow.rb +114 -0
- data/examples/document/mock_executor.rb +80 -0
- data/lib/circuit_breaker/executors/README.md +664 -0
- data/lib/circuit_breaker/executors/agent_executor.rb +187 -0
- data/lib/circuit_breaker/executors/assistant_executor.rb +245 -0
- data/lib/circuit_breaker/executors/base_executor.rb +56 -0
- data/lib/circuit_breaker/executors/docker_executor.rb +56 -0
- data/lib/circuit_breaker/executors/dsl.rb +97 -0
- data/lib/circuit_breaker/executors/llm/memory.rb +82 -0
- data/lib/circuit_breaker/executors/llm/tools.rb +94 -0
- data/lib/circuit_breaker/executors/nats_executor.rb +230 -0
- data/lib/circuit_breaker/executors/serverless_executor.rb +25 -0
- data/lib/circuit_breaker/executors/step_executor.rb +47 -0
- data/lib/circuit_breaker/history.rb +81 -0
- data/lib/circuit_breaker/rules.rb +251 -0
- data/lib/circuit_breaker/templates/mermaid.html.erb +51 -0
- data/lib/circuit_breaker/templates/plantuml.html.erb +55 -0
- data/lib/circuit_breaker/token.rb +486 -0
- data/lib/circuit_breaker/visualizer.rb +173 -0
- data/lib/circuit_breaker/workflow_dsl.rb +359 -0
- data/lib/circuit_breaker.rb +236 -0
- data/workflow-editor/.gitignore +24 -0
- data/workflow-editor/README.md +106 -0
- data/workflow-editor/eslint.config.js +28 -0
- data/workflow-editor/index.html +13 -0
- data/workflow-editor/package-lock.json +6864 -0
- data/workflow-editor/package.json +50 -0
- data/workflow-editor/postcss.config.js +6 -0
- data/workflow-editor/public/vite.svg +1 -0
- data/workflow-editor/src/App.css +42 -0
- data/workflow-editor/src/App.tsx +365 -0
- data/workflow-editor/src/assets/react.svg +1 -0
- data/workflow-editor/src/components/AddNodeButton.tsx +68 -0
- data/workflow-editor/src/components/EdgeDetails.tsx +175 -0
- data/workflow-editor/src/components/NodeDetails.tsx +177 -0
- data/workflow-editor/src/components/ResizablePanel.tsx +74 -0
- data/workflow-editor/src/components/SaveButton.tsx +45 -0
- data/workflow-editor/src/config/change_workflow.yaml +59 -0
- data/workflow-editor/src/config/constants.ts +11 -0
- data/workflow-editor/src/config/flowConfig.ts +189 -0
- data/workflow-editor/src/config/uiConfig.ts +77 -0
- data/workflow-editor/src/config/workflow.yaml +58 -0
- data/workflow-editor/src/hooks/useKeyPress.ts +29 -0
- data/workflow-editor/src/index.css +34 -0
- data/workflow-editor/src/main.tsx +10 -0
- data/workflow-editor/src/server/saveWorkflow.ts +81 -0
- data/workflow-editor/src/utils/saveWorkflow.ts +92 -0
- data/workflow-editor/src/utils/workflowLoader.ts +26 -0
- data/workflow-editor/src/utils/workflowTransformer.ts +91 -0
- data/workflow-editor/src/vite-env.d.ts +1 -0
- data/workflow-editor/src/yaml.d.ts +4 -0
- data/workflow-editor/tailwind.config.js +15 -0
- data/workflow-editor/tsconfig.app.json +26 -0
- data/workflow-editor/tsconfig.json +7 -0
- data/workflow-editor/tsconfig.node.json +24 -0
- data/workflow-editor/vite.config.ts +8 -0
- metadata +267 -0
@@ -0,0 +1,535 @@
|
|
1
|
+
module Examples
|
2
|
+
# Document Assistant using Ollama for content analysis and suggestions
|
3
|
+
class DocumentAssistant
|
4
|
+
class AnalyzeContentTool < CircuitBreaker::Executors::LLM::Tool
|
5
|
+
def initialize
|
6
|
+
super(
|
7
|
+
name: 'analyze_content',
|
8
|
+
description: 'Analyze document content and provide feedback',
|
9
|
+
parameters: {
|
10
|
+
content: { type: 'string', description: 'Document content to analyze' },
|
11
|
+
word_count: { type: 'integer', description: 'Current word count' },
|
12
|
+
min_words: { type: 'integer', description: 'Minimum required words' }
|
13
|
+
}
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
def execute(content:, word_count:, min_words: 100)
|
18
|
+
analysis = []
|
19
|
+
analysis << analyze_length(word_count, min_words)
|
20
|
+
analysis << analyze_structure(content)
|
21
|
+
analysis << analyze_clarity(content)
|
22
|
+
analysis << analyze_completeness(content)
|
23
|
+
analysis.join("\n\n")
|
24
|
+
rescue => e
|
25
|
+
"Error analyzing content: #{e.message}"
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def analyze_length(word_count, min_words)
|
31
|
+
<<~ANALYSIS
|
32
|
+
Word Count Analysis:
|
33
|
+
- Current count: #{word_count} words
|
34
|
+
- Minimum required: #{min_words} words
|
35
|
+
- Status: #{word_count >= min_words ? 'Meets' : 'Does not meet'} requirement
|
36
|
+
ANALYSIS
|
37
|
+
end
|
38
|
+
|
39
|
+
def analyze_structure(content)
|
40
|
+
return "Unable to analyze structure" unless content.is_a?(String)
|
41
|
+
|
42
|
+
paragraphs = content.split(/\n\n+/).size
|
43
|
+
sections = content.scan(/^[A-Z][^.!?]*[:|\n]/).size
|
44
|
+
|
45
|
+
<<~ANALYSIS
|
46
|
+
Structure Analysis:
|
47
|
+
- Paragraphs: #{paragraphs}
|
48
|
+
- Sections: #{sections}
|
49
|
+
- Assessment: #{assess_structure(paragraphs, sections)}
|
50
|
+
ANALYSIS
|
51
|
+
end
|
52
|
+
|
53
|
+
def assess_structure(paragraphs, sections)
|
54
|
+
if paragraphs < 3
|
55
|
+
"Consider adding more paragraphs for better organization"
|
56
|
+
elsif sections < 2
|
57
|
+
"Consider adding section headings for better structure"
|
58
|
+
else
|
59
|
+
"Good structural organization"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def analyze_clarity(content)
|
64
|
+
return "Unable to analyze clarity" unless content.is_a?(String)
|
65
|
+
|
66
|
+
sentences = content.split(/[.!?]/).map(&:strip).reject(&:empty?)
|
67
|
+
long_sentences = sentences.select { |s| s.split.size > 20 }
|
68
|
+
passive_voice = content.scan(/\b(?:is|are|was|were|be|been|being)\s+\w+ed\b/i).size
|
69
|
+
|
70
|
+
<<~ANALYSIS
|
71
|
+
Clarity Analysis:
|
72
|
+
- Long sentences: #{long_sentences.size}
|
73
|
+
- Passive voice instances: #{passive_voice}
|
74
|
+
- Recommendations: #{clarity_recommendations(long_sentences.size, passive_voice)}
|
75
|
+
ANALYSIS
|
76
|
+
end
|
77
|
+
|
78
|
+
def clarity_recommendations(long_sentences, passive_voice)
|
79
|
+
recommendations = []
|
80
|
+
recommendations << "Break down long sentences" if long_sentences > 0
|
81
|
+
recommendations << "Use more active voice" if passive_voice > 2
|
82
|
+
recommendations.empty? ? "Good clarity" : recommendations.join(", ")
|
83
|
+
end
|
84
|
+
|
85
|
+
def analyze_completeness(content)
|
86
|
+
return "Unable to analyze completeness" unless content.is_a?(String)
|
87
|
+
|
88
|
+
key_sections = {
|
89
|
+
introduction: content.downcase.include?('introduction'),
|
90
|
+
background: content.downcase.include?('background'),
|
91
|
+
methodology: content.downcase.include?('methodology'),
|
92
|
+
conclusion: content.downcase.include?('conclusion')
|
93
|
+
}
|
94
|
+
|
95
|
+
<<~ANALYSIS
|
96
|
+
Completeness Analysis:
|
97
|
+
- Key sections present: #{key_sections.count { |_, present| present }}/#{key_sections.size}
|
98
|
+
- Missing sections: #{missing_sections(key_sections)}
|
99
|
+
ANALYSIS
|
100
|
+
end
|
101
|
+
|
102
|
+
def missing_sections(sections)
|
103
|
+
missing = sections.reject { |_, present| present }.keys
|
104
|
+
missing.empty? ? "None" : missing.map(&:to_s).join(", ")
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
class SuggestImprovementsTool < CircuitBreaker::Executors::LLM::Tool
|
109
|
+
def initialize
|
110
|
+
super(
|
111
|
+
name: 'suggest_improvements',
|
112
|
+
description: 'Suggest specific improvements for the document',
|
113
|
+
parameters: {
|
114
|
+
content: { type: 'string', description: 'Document content' },
|
115
|
+
feedback_type: { type: 'string', description: 'Type of feedback (structure/clarity/completeness)' }
|
116
|
+
}
|
117
|
+
)
|
118
|
+
end
|
119
|
+
|
120
|
+
def execute(content:, feedback_type:)
|
121
|
+
case feedback_type.downcase
|
122
|
+
when 'structure'
|
123
|
+
suggest_structure_improvements(content)
|
124
|
+
when 'clarity'
|
125
|
+
suggest_clarity_improvements(content)
|
126
|
+
when 'completeness'
|
127
|
+
suggest_completeness_improvements(content)
|
128
|
+
else
|
129
|
+
"Unknown feedback type: #{feedback_type}"
|
130
|
+
end
|
131
|
+
rescue => e
|
132
|
+
"Error suggesting improvements: #{e.message}"
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
|
137
|
+
def suggest_structure_improvements(content)
|
138
|
+
return "Unable to analyze structure" unless content.is_a?(String)
|
139
|
+
|
140
|
+
paragraphs = content.split(/\n\n+/)
|
141
|
+
suggestions = []
|
142
|
+
|
143
|
+
suggestions << structure_organization_suggestions(paragraphs)
|
144
|
+
suggestions << structure_formatting_suggestions(content)
|
145
|
+
suggestions << structure_transition_suggestions(paragraphs)
|
146
|
+
|
147
|
+
format_suggestions("Structure Improvements", suggestions.flatten)
|
148
|
+
end
|
149
|
+
|
150
|
+
def structure_organization_suggestions(paragraphs)
|
151
|
+
suggestions = []
|
152
|
+
suggestions << "Break content into more sections" if paragraphs.size < 3
|
153
|
+
suggestions << "Add clear section headings" unless content.match?(/^#|^[A-Z].*:/)
|
154
|
+
suggestions
|
155
|
+
end
|
156
|
+
|
157
|
+
def structure_formatting_suggestions(content)
|
158
|
+
suggestions = []
|
159
|
+
suggestions << "Use bullet points for lists" unless content.match?(/^\s*[-*]/)
|
160
|
+
suggestions << "Add emphasis using bold or italics" unless content.match?(/\*\*|\*/)
|
161
|
+
suggestions
|
162
|
+
end
|
163
|
+
|
164
|
+
def structure_transition_suggestions(paragraphs)
|
165
|
+
return [] if paragraphs.size <= 1
|
166
|
+
["Add transition sentences between paragraphs"]
|
167
|
+
end
|
168
|
+
|
169
|
+
def suggest_clarity_improvements(content)
|
170
|
+
return "Unable to analyze clarity" unless content.is_a?(String)
|
171
|
+
|
172
|
+
sentences = content.split(/[.!?]/).map(&:strip).reject(&:empty?)
|
173
|
+
suggestions = []
|
174
|
+
|
175
|
+
suggestions << clarity_sentence_suggestions(sentences)
|
176
|
+
suggestions << clarity_voice_suggestions(content)
|
177
|
+
suggestions << clarity_word_choice_suggestions(content)
|
178
|
+
|
179
|
+
format_suggestions("Clarity Improvements", suggestions.flatten)
|
180
|
+
end
|
181
|
+
|
182
|
+
def clarity_sentence_suggestions(sentences)
|
183
|
+
suggestions = []
|
184
|
+
if sentences.any? { |s| s.split.size > 20 }
|
185
|
+
suggestions << "Break down long sentences into shorter ones"
|
186
|
+
suggestions << "Use more punctuation to improve readability"
|
187
|
+
end
|
188
|
+
suggestions
|
189
|
+
end
|
190
|
+
|
191
|
+
def clarity_voice_suggestions(content)
|
192
|
+
suggestions = []
|
193
|
+
if content.match?(/\b(?:is|are|was|were)\s+\w+ed\b/i)
|
194
|
+
suggestions << "Use active voice instead of passive voice"
|
195
|
+
suggestions << "Make sentences more direct"
|
196
|
+
end
|
197
|
+
suggestions
|
198
|
+
end
|
199
|
+
|
200
|
+
def clarity_word_choice_suggestions(content)
|
201
|
+
suggestions = []
|
202
|
+
if content.match?(/\b(?:very|really|quite|extremely)\b/i)
|
203
|
+
suggestions << "Use stronger, more specific words"
|
204
|
+
suggestions << "Remove unnecessary intensifiers"
|
205
|
+
end
|
206
|
+
suggestions
|
207
|
+
end
|
208
|
+
|
209
|
+
def suggest_completeness_improvements(content)
|
210
|
+
return "Unable to analyze completeness" unless content.is_a?(String)
|
211
|
+
|
212
|
+
suggestions = []
|
213
|
+
suggestions << completeness_section_suggestions(content)
|
214
|
+
suggestions << completeness_detail_suggestions(content)
|
215
|
+
suggestions << completeness_reference_suggestions(content)
|
216
|
+
|
217
|
+
format_suggestions("Completeness Improvements", suggestions.flatten)
|
218
|
+
end
|
219
|
+
|
220
|
+
def completeness_section_suggestions(content)
|
221
|
+
suggestions = []
|
222
|
+
%w[introduction background methodology results conclusion].each do |section|
|
223
|
+
suggestions << "Add #{section} section" unless content.downcase.include?(section)
|
224
|
+
end
|
225
|
+
suggestions
|
226
|
+
end
|
227
|
+
|
228
|
+
def completeness_detail_suggestions(content)
|
229
|
+
suggestions = []
|
230
|
+
suggestions << "Include more specific examples" if content.scan(/(?:for example|such as|like)/).size < 2
|
231
|
+
suggestions << "Add supporting data or evidence" if content.scan(/\d+%|\d+\.\d+/).size < 2
|
232
|
+
suggestions
|
233
|
+
end
|
234
|
+
|
235
|
+
def completeness_reference_suggestions(content)
|
236
|
+
suggestions = []
|
237
|
+
suggestions << "Add citations or references" unless content.match?(/\[\d+\]|\(\w+,\s*\d{4}\)/)
|
238
|
+
suggestions
|
239
|
+
end
|
240
|
+
|
241
|
+
def format_suggestions(title, suggestions)
|
242
|
+
return "No improvements needed" if suggestions.empty?
|
243
|
+
|
244
|
+
[
|
245
|
+
title,
|
246
|
+
"=" * title.length,
|
247
|
+
"",
|
248
|
+
suggestions.map { |s| "- #{s}" }
|
249
|
+
].flatten.join("\n")
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
class SentimentAnalysisTool < CircuitBreaker::Executors::LLM::Tool
|
254
|
+
def initialize
|
255
|
+
super(
|
256
|
+
name: 'analyze_sentiment',
|
257
|
+
description: 'Analyze the sentiment and tone of the document',
|
258
|
+
parameters: {
|
259
|
+
content: { type: 'string', description: 'Document content to analyze' },
|
260
|
+
context: { type: 'string', description: 'Document context or type', default: 'general' }
|
261
|
+
}
|
262
|
+
)
|
263
|
+
end
|
264
|
+
|
265
|
+
def execute(content:, context: 'general')
|
266
|
+
return "Unable to analyze sentiment" unless content.is_a?(String)
|
267
|
+
|
268
|
+
analysis = []
|
269
|
+
analysis << analyze_overall_sentiment(content)
|
270
|
+
analysis << analyze_emotional_tone(content)
|
271
|
+
analysis << analyze_formality(content)
|
272
|
+
analysis << context_specific_analysis(content, context)
|
273
|
+
analysis.join("\n\n")
|
274
|
+
rescue => e
|
275
|
+
"Error analyzing sentiment: #{e.message}"
|
276
|
+
end
|
277
|
+
|
278
|
+
private
|
279
|
+
|
280
|
+
def analyze_overall_sentiment(content)
|
281
|
+
# Simple sentiment indicators
|
282
|
+
positive_words = %w[good great excellent amazing wonderful positive success successful achieve accomplished beneficial effective]
|
283
|
+
negative_words = %w[bad poor terrible horrible negative fail failure difficult problem issue concern]
|
284
|
+
|
285
|
+
positive_count = count_matches(content, positive_words)
|
286
|
+
negative_count = count_matches(content, negative_words)
|
287
|
+
|
288
|
+
sentiment_score = calculate_sentiment_score(positive_count, negative_count)
|
289
|
+
|
290
|
+
<<~ANALYSIS
|
291
|
+
Overall Sentiment Analysis:
|
292
|
+
- Sentiment Score: #{sentiment_score}/10
|
293
|
+
- Positive Indicators: #{positive_count}
|
294
|
+
- Negative Indicators: #{negative_count}
|
295
|
+
- Overall Tone: #{describe_sentiment(sentiment_score)}
|
296
|
+
ANALYSIS
|
297
|
+
end
|
298
|
+
|
299
|
+
def analyze_emotional_tone(content)
|
300
|
+
emotional_indicators = {
|
301
|
+
confidence: %w[confident assured certain definitely clearly],
|
302
|
+
uncertainty: %w[maybe perhaps possibly might potentially],
|
303
|
+
urgency: %w[urgent immediate critical essential crucial],
|
304
|
+
caution: %w[careful cautious warning risk concern]
|
305
|
+
}
|
306
|
+
|
307
|
+
tone_analysis = emotional_indicators.transform_values { |words| count_matches(content, words) }
|
308
|
+
|
309
|
+
<<~ANALYSIS
|
310
|
+
Emotional Tone Analysis:
|
311
|
+
- Confidence Level: #{describe_intensity(tone_analysis[:confidence])}
|
312
|
+
- Uncertainty Level: #{describe_intensity(tone_analysis[:uncertainty])}
|
313
|
+
- Urgency Level: #{describe_intensity(tone_analysis[:urgency])}
|
314
|
+
- Caution Level: #{describe_intensity(tone_analysis[:caution])}
|
315
|
+
ANALYSIS
|
316
|
+
end
|
317
|
+
|
318
|
+
def analyze_formality(content)
|
319
|
+
formal_indicators = %w[furthermore moreover consequently therefore thus accordingly]
|
320
|
+
informal_indicators = %w[like well anyway actually basically]
|
321
|
+
|
322
|
+
formal_count = count_matches(content, formal_indicators)
|
323
|
+
informal_count = count_matches(content, informal_indicators)
|
324
|
+
|
325
|
+
formality_level = calculate_formality_level(formal_count, informal_count)
|
326
|
+
|
327
|
+
<<~ANALYSIS
|
328
|
+
Formality Analysis:
|
329
|
+
- Formality Level: #{formality_level}
|
330
|
+
- Formal Language Usage: #{describe_intensity(formal_count)}
|
331
|
+
- Informal Language Usage: #{describe_intensity(informal_count)}
|
332
|
+
#{formality_recommendations(formality_level)}
|
333
|
+
ANALYSIS
|
334
|
+
end
|
335
|
+
|
336
|
+
def context_specific_analysis(content, context)
|
337
|
+
case context.downcase
|
338
|
+
when 'technical'
|
339
|
+
analyze_technical_tone(content)
|
340
|
+
when 'business'
|
341
|
+
analyze_business_tone(content)
|
342
|
+
when 'academic'
|
343
|
+
analyze_academic_tone(content)
|
344
|
+
else
|
345
|
+
analyze_general_tone(content)
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
private
|
350
|
+
|
351
|
+
def count_matches(content, words)
|
352
|
+
words.sum { |word| content.downcase.scan(/\b#{word}\b/).count }
|
353
|
+
end
|
354
|
+
|
355
|
+
def calculate_sentiment_score(positive, negative)
|
356
|
+
return 5 if positive == 0 && negative == 0
|
357
|
+
total = positive + negative
|
358
|
+
return 5 if total == 0
|
359
|
+
((positive.to_f / total) * 10).round(1)
|
360
|
+
end
|
361
|
+
|
362
|
+
def describe_sentiment(score)
|
363
|
+
case score
|
364
|
+
when 0..3 then "Strongly Negative"
|
365
|
+
when 3..4 then "Negative"
|
366
|
+
when 4..6 then "Neutral"
|
367
|
+
when 6..8 then "Positive"
|
368
|
+
else "Strongly Positive"
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
def describe_intensity(count)
|
373
|
+
case count
|
374
|
+
when 0 then "Very Low"
|
375
|
+
when 1..2 then "Low"
|
376
|
+
when 3..5 then "Moderate"
|
377
|
+
when 6..8 then "High"
|
378
|
+
else "Very High"
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
def calculate_formality_level(formal, informal)
|
383
|
+
total = formal + informal
|
384
|
+
return "Neutral" if total == 0
|
385
|
+
|
386
|
+
ratio = formal.to_f / (formal + informal)
|
387
|
+
case ratio
|
388
|
+
when 0..0.2 then "Very Informal"
|
389
|
+
when 0.2..0.4 then "Informal"
|
390
|
+
when 0.4..0.6 then "Neutral"
|
391
|
+
when 0.6..0.8 then "Formal"
|
392
|
+
else "Very Formal"
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
def formality_recommendations(level)
|
397
|
+
case level
|
398
|
+
when "Very Informal"
|
399
|
+
"Recommendation: Consider using more formal language for professional documents"
|
400
|
+
when "Informal"
|
401
|
+
"Recommendation: Slightly increase formal language usage"
|
402
|
+
when "Very Formal"
|
403
|
+
"Recommendation: Consider adding some less formal elements for better engagement"
|
404
|
+
else
|
405
|
+
"Recommendation: Current formality level is appropriate"
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
def analyze_technical_tone(content)
|
410
|
+
technical_terms = %w[implementation algorithm framework architecture database api interface]
|
411
|
+
jargon_count = count_matches(content, technical_terms)
|
412
|
+
|
413
|
+
<<~ANALYSIS
|
414
|
+
Technical Context Analysis:
|
415
|
+
- Technical Term Usage: #{describe_intensity(jargon_count)}
|
416
|
+
- Technical Depth: #{jargon_count > 5 ? "High" : "Moderate"}
|
417
|
+
ANALYSIS
|
418
|
+
end
|
419
|
+
|
420
|
+
def analyze_business_tone(content)
|
421
|
+
business_terms = %w[roi stakeholder deliverable milestone objective strategic]
|
422
|
+
business_focus = count_matches(content, business_terms)
|
423
|
+
|
424
|
+
<<~ANALYSIS
|
425
|
+
Business Context Analysis:
|
426
|
+
- Business Term Usage: #{describe_intensity(business_focus)}
|
427
|
+
- Business Focus: #{business_focus > 5 ? "Strong" : "Moderate"}
|
428
|
+
ANALYSIS
|
429
|
+
end
|
430
|
+
|
431
|
+
def analyze_academic_tone(content)
|
432
|
+
academic_terms = %w[research study analysis methodology hypothesis conclusion evidence]
|
433
|
+
academic_focus = count_matches(content, academic_terms)
|
434
|
+
|
435
|
+
<<~ANALYSIS
|
436
|
+
Academic Context Analysis:
|
437
|
+
- Academic Term Usage: #{describe_intensity(academic_focus)}
|
438
|
+
- Academic Rigor: #{academic_focus > 5 ? "High" : "Moderate"}
|
439
|
+
ANALYSIS
|
440
|
+
end
|
441
|
+
|
442
|
+
def analyze_general_tone(content)
|
443
|
+
<<~ANALYSIS
|
444
|
+
General Tone Analysis:
|
445
|
+
- Accessibility: #{analyze_accessibility(content)}
|
446
|
+
- Engagement Level: #{analyze_engagement(content)}
|
447
|
+
ANALYSIS
|
448
|
+
end
|
449
|
+
|
450
|
+
def analyze_accessibility(content)
|
451
|
+
complex_words = content.scan(/\b\w{12,}\b/).size
|
452
|
+
average_sentence_length = content.split(/[.!?]/).map(&:split).map(&:size).sum.to_f / content.split(/[.!?]/).size
|
453
|
+
|
454
|
+
if complex_words > 5 || average_sentence_length > 20
|
455
|
+
"Complex - Consider simplifying"
|
456
|
+
else
|
457
|
+
"Good - Easy to understand"
|
458
|
+
end
|
459
|
+
end
|
460
|
+
|
461
|
+
def analyze_engagement(content)
|
462
|
+
engagement_markers = %w[you your we our they them it his her imagine consider note]
|
463
|
+
engagement_level = count_matches(content, engagement_markers)
|
464
|
+
|
465
|
+
describe_intensity(engagement_level)
|
466
|
+
end
|
467
|
+
end
|
468
|
+
|
469
|
+
class << self
|
470
|
+
def define(&block)
|
471
|
+
new.tap { |assistant| assistant.instance_eval(&block) if block_given? }
|
472
|
+
end
|
473
|
+
end
|
474
|
+
|
475
|
+
def initialize(model = 'qwen2.5-coder')
|
476
|
+
@executor = CircuitBreaker::Executors::AssistantExecutor.define do
|
477
|
+
use_model model
|
478
|
+
with_system_prompt "You are a document review assistant. You help analyze documents and provide constructive feedback."
|
479
|
+
with_parameters(
|
480
|
+
temperature: 0.7,
|
481
|
+
top_p: 0.9,
|
482
|
+
top_k: 40
|
483
|
+
)
|
484
|
+
add_tools [
|
485
|
+
AnalyzeContentTool.new,
|
486
|
+
SuggestImprovementsTool.new,
|
487
|
+
SentimentAnalysisTool.new
|
488
|
+
]
|
489
|
+
end
|
490
|
+
end
|
491
|
+
|
492
|
+
def analyze_document(token)
|
493
|
+
@executor
|
494
|
+
.update_context(input: generate_analysis_prompt(token))
|
495
|
+
.execute
|
496
|
+
|
497
|
+
result = @executor.result
|
498
|
+
if result && result[:output]
|
499
|
+
result[:output][:content]
|
500
|
+
else
|
501
|
+
"Error analyzing document"
|
502
|
+
end
|
503
|
+
end
|
504
|
+
|
505
|
+
private
|
506
|
+
|
507
|
+
def generate_analysis_prompt(token)
|
508
|
+
<<~PROMPT
|
509
|
+
Please analyze this document and provide feedback:
|
510
|
+
|
511
|
+
Title: #{token.title}
|
512
|
+
Content: #{token.content}
|
513
|
+
Current Word Count: #{token.word_count}
|
514
|
+
Priority: #{token.priority}
|
515
|
+
Context: #{determine_document_context(token)}
|
516
|
+
|
517
|
+
Please:
|
518
|
+
1. Analyze the content quality and structure
|
519
|
+
2. Analyze the sentiment and tone
|
520
|
+
3. Suggest specific improvements
|
521
|
+
4. Check if it meets minimum length requirements
|
522
|
+
5. Provide any additional recommendations
|
523
|
+
|
524
|
+
Use the available tools to perform the analysis.
|
525
|
+
PROMPT
|
526
|
+
end
|
527
|
+
|
528
|
+
def determine_document_context(token)
|
529
|
+
return 'technical' if token.content.downcase.match?(/\b(?:code|algorithm|implementation|api|database)\b/)
|
530
|
+
return 'business' if token.content.downcase.match?(/\b(?:roi|stakeholder|business|market|strategy)\b/)
|
531
|
+
return 'academic' if token.content.downcase.match?(/\b(?:research|study|hypothesis|methodology)\b/)
|
532
|
+
'general'
|
533
|
+
end
|
534
|
+
end
|
535
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require_relative '../../lib/circuit_breaker'
|
2
|
+
|
3
|
+
module Examples
|
4
|
+
module Document
|
5
|
+
module Rules
|
6
|
+
def self.define
|
7
|
+
CircuitBreaker::Rules::DSL.define do
|
8
|
+
# Content validation rules
|
9
|
+
rule :valid_word_count do |token|
|
10
|
+
analysis = context.get_result(:analysis)
|
11
|
+
analysis && analysis[:word_count] >= 50
|
12
|
+
end
|
13
|
+
|
14
|
+
rule :valid_clarity do |token|
|
15
|
+
clarity = context.get_result(:clarity)
|
16
|
+
clarity && clarity[:score] >= 70
|
17
|
+
end
|
18
|
+
|
19
|
+
rule :valid_completeness do |token|
|
20
|
+
completeness = context.get_result(:completeness)
|
21
|
+
completeness && completeness[:score] >= 80
|
22
|
+
end
|
23
|
+
|
24
|
+
# Review rules
|
25
|
+
rule :valid_review_metrics do |token|
|
26
|
+
review = context.get_result(:review)
|
27
|
+
review && review[:metrics][:word_count] >= 50 &&
|
28
|
+
review[:metrics][:clarity_score] >= 70 &&
|
29
|
+
review[:metrics][:completeness_score] >= 80
|
30
|
+
end
|
31
|
+
|
32
|
+
rule :is_high_priority do |token|
|
33
|
+
token.priority == "high"
|
34
|
+
end
|
35
|
+
|
36
|
+
rule :is_urgent do |token|
|
37
|
+
token.priority == "urgent"
|
38
|
+
end
|
39
|
+
|
40
|
+
# Approval rules
|
41
|
+
rule :valid_approver do |token|
|
42
|
+
final = context.get_result(:final)
|
43
|
+
final && final[:status] == "APPROVED"
|
44
|
+
end
|
45
|
+
|
46
|
+
rule :approved_status do |token|
|
47
|
+
final = context.get_result(:final)
|
48
|
+
final && final[:status] == "APPROVED"
|
49
|
+
end
|
50
|
+
|
51
|
+
# Rejection rules
|
52
|
+
rule :has_rejection_reasons do |token|
|
53
|
+
rejection = context.get_result(:rejection)
|
54
|
+
rejection && !rejection[:reasons].empty?
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
require_relative '../../lib/circuit_breaker/token'
|
3
|
+
|
4
|
+
module Examples
|
5
|
+
module Document
|
6
|
+
class DocumentToken < CircuitBreaker::Token
|
7
|
+
# Define valid states
|
8
|
+
states :draft, :pending_review, :reviewed, :approved, :rejected
|
9
|
+
|
10
|
+
# Define document attributes with types
|
11
|
+
attribute :title, String
|
12
|
+
attribute :content, String
|
13
|
+
attribute :author_id, String
|
14
|
+
attribute :reviewer_id, String
|
15
|
+
attribute :approver_id, String
|
16
|
+
attribute :reviewer_comments, String
|
17
|
+
attribute :rejection_reason, String
|
18
|
+
attribute :tags, Array
|
19
|
+
attribute :priority, String, allowed: %w[low medium high urgent]
|
20
|
+
attribute :due_date, Time
|
21
|
+
attribute :word_count, Integer
|
22
|
+
attribute :external_url, String
|
23
|
+
|
24
|
+
# Define timestamp fields and state messages
|
25
|
+
state_configs do
|
26
|
+
state :pending_review,
|
27
|
+
timestamps: :submitted_at,
|
28
|
+
message: ->(t) { "Document submitted for review by #{t.reviewer_id}" }
|
29
|
+
|
30
|
+
state :reviewed,
|
31
|
+
timestamps: :reviewed_at,
|
32
|
+
message: ->(t) { "Document reviewed by #{t.reviewer_id} with comments" }
|
33
|
+
|
34
|
+
state :approved,
|
35
|
+
timestamps: :approved_at,
|
36
|
+
message: ->(t) { "Document approved by #{t.approver_id}" }
|
37
|
+
|
38
|
+
state :rejected,
|
39
|
+
timestamps: :rejected_at,
|
40
|
+
message: ->(t) { "Document rejected with reason: #{t.rejection_reason}" }
|
41
|
+
|
42
|
+
# Handle shared timestamp
|
43
|
+
on_states [:approved, :rejected], timestamps: :completed_at
|
44
|
+
end
|
45
|
+
|
46
|
+
def initialize
|
47
|
+
@id = SecureRandom.uuid
|
48
|
+
@state = :draft
|
49
|
+
@content = <<~CONTENT
|
50
|
+
Introduction
|
51
|
+
This is a detailed document that shows our workflow system. We aim to create
|
52
|
+
a robust and clear process for managing documents effectively.
|
53
|
+
|
54
|
+
Background
|
55
|
+
We need to ensure documents meet quality standards. This means checking the
|
56
|
+
word count, clarity of writing, and completeness of content. Each document
|
57
|
+
goes through several stages of review.
|
58
|
+
|
59
|
+
The process uses smart tools to check document quality. These tools look at
|
60
|
+
various aspects like word count and readability. They help make sure our
|
61
|
+
documents are clear and complete.
|
62
|
+
|
63
|
+
Conclusion
|
64
|
+
By following these rules and steps, we keep our documents clear and useful.
|
65
|
+
The workflow helps us maintain high standards and ensures good quality.
|
66
|
+
Thank you for reading this example document.
|
67
|
+
CONTENT
|
68
|
+
@priority = "high"
|
69
|
+
@created_at = Time.now
|
70
|
+
@updated_at = Time.now
|
71
|
+
@history = []
|
72
|
+
end
|
73
|
+
|
74
|
+
def to_json(include_private = false)
|
75
|
+
JSON.pretty_generate(to_h(include_private))
|
76
|
+
end
|
77
|
+
|
78
|
+
def to_yaml(include_private = false)
|
79
|
+
to_h(include_private).to_yaml
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|