llm_conductor 0.1.1 → 1.0.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c1b006030ac01906369f830f2c11d958f40c3bf3a48404179d78c6b1e75a82e5
4
- data.tar.gz: 7ccf09702230675a381f164f4ccb91034aec880c5769cb8887711c6737385c46
3
+ metadata.gz: 00742db531ea95e6247acbb8b7ea7e92619df851b0d558fd33a4f20d8cf23873
4
+ data.tar.gz: 9492982bc533d2552b9b55f8e0892fc523c2340d75086ade217651bf9d72730a
5
5
  SHA512:
6
- metadata.gz: 4c3283602c064e5c7e67ac5a1b26353c936b9d5d351ad21d87a89058aa045af1f8f916f350ac07db4a0405a6fe9091b09ecc00b091afd57039d3ae560bd199e8
7
- data.tar.gz: 571400d81fa8029ecf9f9d4a53c48ac26392c13de927994c358b0e953d27257b94ff929f1d65f13b64ccddaada8785e33f271b4cf99ebdad74c63abef3dd4a97
6
+ metadata.gz: 8d23aa8d06cc823d77ec4992605208f693a592971d13f88266d587cf06fe78cd42111b7904404cc998215736df1d2b9acf452c5474d070401619d6774a870993
7
+ data.tar.gz: 154240f949c96860372b6cc6eb097267c1a4e9a52b6da968dc30f53d18069f4034fdadfe12fe38b51e5be99d54f75988f1aa48864b624635ce63077bc7f8da6f
data/.rubocop.yml CHANGED
@@ -2,8 +2,6 @@
2
2
  plugins:
3
3
  - rubocop-performance
4
4
  - rubocop-rake
5
-
6
- require:
7
5
  - rubocop-capybara
8
6
  - rubocop-factory_bot
9
7
  - rubocop-rspec
@@ -27,21 +25,14 @@ Style/StringLiteralsInInterpolation:
27
25
 
28
26
  Style/HashSyntax:
29
27
  EnforcedShorthandSyntax: always
30
- # This is not rails application.
31
- # Rails/Blank:
32
- # Enabled: false
33
-
34
- # Rails/Present:
35
- # Enabled: false
36
-
37
- # Rails/TimeZone:
38
- # Enabled: false
39
28
 
40
29
  Lint/ConstantDefinitionInBlock:
41
30
  Enabled: false
42
31
 
43
32
  Metrics/MethodLength:
44
33
  Max: 15
34
+ Exclude:
35
+ - 'lib/llm_conductor/prompts.rb'
45
36
 
46
37
  RSpec/ExampleLength:
47
38
  Enabled: false
@@ -67,7 +58,7 @@ RSpec/MultipleDescribes:
67
58
  RSpec/SpecFilePathFormat:
68
59
  Enabled: false
69
60
 
70
- RSpec/FilePath:
61
+ RSpec/SpecFilePathSuffix:
71
62
  Enabled: false
72
63
 
73
64
  RSpec/UnspecifiedException:
@@ -94,6 +85,19 @@ Metrics/BlockLength:
94
85
  Exclude:
95
86
  - '*.gemspec'
96
87
 
88
+ # Prompt template methods naturally have high complexity due to conditional string building
89
+ Metrics/AbcSize:
90
+ Exclude:
91
+ - 'lib/llm_conductor/prompts.rb'
92
+
93
+ Metrics/CyclomaticComplexity:
94
+ Exclude:
95
+ - 'lib/llm_conductor/prompts.rb'
96
+
97
+ Metrics/PerceivedComplexity:
98
+ Exclude:
99
+ - 'lib/llm_conductor/prompts.rb'
100
+
97
101
  Layout/LineLength:
98
102
  Max: 120
99
103
 
data/README.md CHANGED
@@ -52,14 +52,18 @@ puts response.estimated_cost # Cost in USD
52
52
  ### 2. Template-Based Generation
53
53
 
54
54
  ```ruby
55
- # Use built-in templates with structured data
55
+ # Use built-in text summarization template
56
56
  response = LlmConductor.generate(
57
57
  model: 'gpt-5-mini',
58
- type: :summarize_description,
58
+ type: :summarize_text,
59
59
  data: {
60
- name: 'Ekohe',
61
- domain_name: 'ekohe.com',
62
- description: 'An AI company specializing in...'
60
+ text: 'Ekohe (ee-koh-hee) means "boundless possibility." Our way is to make AI practical, achievable, and most importantly, useful for you — and we prove it every day. With almost 16 years of wins under our belt, a market-leading 24-hr design & development cycle, and 5 offices in the most vibrant cities in the world, we surf the seas of innovation. We create efficient, elegant, and scalable digital products — delivering the right interactive solutions to achieve your audience and business goals. We help you transform. We break new ground across the globe — from AI and ML automation that drives the enterprise, to innovative customer experiences and mobile apps for startups. Our special sauce is the care, curiosity, and dedication we offer to solve for your needs. We focus on your success and deliver the most impactful experiences in the most efficient manner. Our clients tell us we partner with them in a trusted and capable way, driving the right design and technical choices.',
61
+ max_length: '20 words',
62
+ style: 'professional and engaging',
63
+ focus_areas: ['core business', 'expertise', 'target market'],
64
+ audience: 'potential investors',
65
+ include_key_points: true,
66
+ output_format: 'paragraph'
63
67
  }
64
68
  )
65
69
 
@@ -67,7 +71,7 @@ response = LlmConductor.generate(
67
71
  if response.success?
68
72
  puts "Generated: #{response.output}"
69
73
  puts "Tokens: #{response.total_tokens}"
70
- puts "Cost: $#{response.estimated_cost}"
74
+ puts "Cost: $#{response.estimated_cost || 'N/A (free model)'}"
71
75
  else
72
76
  puts "Error: #{response.metadata[:error]}"
73
77
  end
@@ -105,6 +109,33 @@ LlmConductor.configure do |config|
105
109
  config.ollama(
106
110
  base_url: ENV['OLLAMA_ADDRESS'] || 'http://localhost:11434'
107
111
  )
112
+
113
+ # Optional: Configure custom logger
114
+ config.logger = Logger.new($stdout) # Log to stdout
115
+ config.logger = Logger.new('log/llm_conductor.log') # Log to file
116
+ config.logger = Rails.logger # Use Rails logger (in Rails apps)
117
+ end
118
+ ```
119
+
120
+ ### Logging Configuration
121
+
122
+ LLM Conductor supports flexible logging using Ruby's built-in Logger class. By default, when a logger is configured, it uses the DEBUG log level to provide detailed information during development.
123
+
124
+ ```ruby
125
+ LlmConductor.configure do |config|
126
+ # Option 1: Log to stdout - uses DEBUG level by default
127
+ config.logger = Logger.new($stdout)
128
+
129
+ # Option 2: Log to file - set appropriate level
130
+ config.logger = Logger.new('log/llm_conductor.log')
131
+
132
+ # Option 3: Use Rails logger (Rails apps)
133
+ config.logger = Rails.logger
134
+
135
+ # Option 4: Custom logger with formatting
136
+ config.logger = Logger.new($stderr).tap do |logger|
137
+ logger.formatter = proc { |severity, datetime, progname, msg| "#{msg}\n" }
138
+ end
108
139
  end
109
140
  ```
110
141
 
@@ -143,20 +174,6 @@ response = LlmConductor.generate(
143
174
  )
144
175
  ```
145
176
 
146
- **Supported Claude Models:**
147
- - `claude-3-5-sonnet-20241022` (Latest Claude 3.5 Sonnet)
148
- - `claude-3-5-haiku-20241022` (Claude 3.5 Haiku)
149
- - `claude-3-opus-20240229` (Claude 3 Opus)
150
- - `claude-3-sonnet-20240229` (Claude 3 Sonnet)
151
- - `claude-3-haiku-20240307` (Claude 3 Haiku)
152
-
153
- **Why Choose Claude?**
154
- - **Superior Reasoning**: Excellent for complex analysis and problem-solving
155
- - **Code Generation**: Outstanding performance for programming tasks
156
- - **Long Context**: Support for large documents and conversations
157
- - **Safety**: Built with safety and helpfulness in mind
158
- - **Cost Effective**: Competitive pricing for high-quality outputs
159
-
160
177
  ### Google Gemini (Automatic for Gemini models)
161
178
  ```ruby
162
179
  response = LlmConductor.generate(
@@ -172,18 +189,6 @@ response = LlmConductor.generate(
172
189
  )
173
190
  ```
174
191
 
175
- **Supported Gemini Models:**
176
- - `gemini-2.5-flash` (Latest Gemini 2.5 Flash)
177
- - `gemini-2.5-flash` (Gemini 2.5 Flash)
178
- - `gemini-2.0-flash` (Gemini 2.0 Flash)
179
-
180
- **Why Choose Gemini?**
181
- - **Multimodal**: Native support for text, images, and other modalities
182
- - **Long Context**: Massive context windows for large documents
183
- - **Fast Performance**: Optimized for speed and efficiency
184
- - **Google Integration**: Seamless integration with Google services
185
- - **Competitive Pricing**: Cost-effective for high-volume usage
186
-
187
192
  ### Ollama (Default for non-GPT/Claude/Gemini models)
188
193
  ```ruby
189
194
  response = LlmConductor.generate(
@@ -10,7 +10,8 @@ LlmConductor.configure do |config|
10
10
  config.timeout = 30
11
11
  config.max_retries = 3
12
12
  config.retry_delay = 1.0
13
-
13
+ # Use Ruby's built-in Logger class directly
14
+ config.logger = Logger.new($stdout)
14
15
  # Configure providers
15
16
  config.openai(
16
17
  api_key: ENV['OPENAI_API_KEY'],
@@ -24,6 +24,12 @@ module LlmConductor
24
24
  output_text = generate_content(prompt)
25
25
  output_tokens = calculate_tokens(output_text || '')
26
26
 
27
+ # Logging AI request metadata if logger is set
28
+ configuration.logger&.debug(
29
+ "Vendor: #{vendor_name}, Model: #{@model} " \
30
+ "Output_tokens: #{output_tokens} Input_tokens: #{input_tokens}"
31
+ )
32
+
27
33
  build_response(output_text, input_tokens, output_tokens, { prompt: })
28
34
  rescue StandardError => e
29
35
  build_error_response(e)
@@ -35,6 +41,12 @@ module LlmConductor
35
41
  output_text = generate_content(prompt)
36
42
  output_tokens = calculate_tokens(output_text || '')
37
43
 
44
+ # Logging AI request metadata if logger is set
45
+ configuration.logger&.debug(
46
+ "Vendor: #{vendor_name}, Model: #{@model} " \
47
+ "Output_tokens: #{output_tokens} Input_tokens: #{input_tokens}"
48
+ )
49
+
38
50
  build_response(output_text, input_tokens, output_tokens)
39
51
  rescue StandardError => e
40
52
  build_error_response(e)
@@ -89,10 +101,18 @@ module LlmConductor
89
101
  # Build metadata for the response
90
102
  def build_metadata
91
103
  {
92
- vendor: self.class.name.split('::').last.gsub('Client', '').downcase.to_sym,
104
+ vendor: vendor_name,
93
105
  timestamp: Time.now.utc.iso8601
94
106
  }
95
107
  end
108
+
109
+ def vendor_name
110
+ self.class.name.split('::').last.gsub('Client', '').downcase.to_sym
111
+ end
112
+
113
+ def configuration
114
+ LlmConductor.configuration
115
+ end
96
116
  end
97
117
  end
98
118
  end
@@ -4,7 +4,7 @@
4
4
  module LlmConductor
5
5
  # Configuration class for managing API keys, endpoints, and default settings
6
6
  class Configuration
7
- attr_accessor :default_model, :default_vendor, :timeout, :max_retries, :retry_delay
7
+ attr_accessor :default_model, :default_vendor, :timeout, :max_retries, :retry_delay, :logger
8
8
  attr_reader :providers
9
9
 
10
10
  def initialize
@@ -14,6 +14,7 @@ module LlmConductor
14
14
  @timeout = 30
15
15
  @max_retries = 3
16
16
  @retry_delay = 1.0
17
+ @logger = nil
17
18
 
18
19
  # Provider configurations
19
20
  @providers = {}
@@ -1,124 +1,140 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LlmConductor
4
- # Collection of pre-built prompt templates for common LLM tasks including
5
- # content analysis, link extraction, and data summarization.
4
+ # Collection of general-purpose prompt templates for common LLM tasks
6
5
  module Prompts
7
- def prompt_featured_links(data)
6
+ # General prompt for extracting links from HTML content
7
+ # More flexible and applicable to various use cases
8
+ def prompt_extract_links(data)
9
+ criteria = data[:criteria] || 'relevant and useful'
10
+ max_links = data[:max_links] || 10
11
+ link_types = data[:link_types] || %w[navigation content footer]
12
+
8
13
  <<~PROMPT
9
- You are an AI assistant tasked with analyzing a webpage's HTML content to extract the most valuable links. Your goal is to identify links related to features, products, solutions, pricing, and social media profiles, prioritizing those from the same domain as the current page. Here are your instructions:
14
+ Analyze the provided HTML content and extract links based on the specified criteria.
15
+
16
+ HTML Content:
17
+ #{data[:html_content] || data[:htmls]}
18
+
19
+ Extraction Criteria: #{criteria}
20
+ Maximum Links: #{max_links}
21
+ Link Types to Consider: #{link_types.join(', ')}
10
22
 
11
- - You will be provided with the HTML content of the current page in the following format:
12
- <page_html>
13
- #{data[:htmls]}
14
- </page_html>
23
+ #{"Domain Filter: Only include links from domain #{data[:domain_filter]}" if data[:domain_filter]}
15
24
 
16
- - Parse the HTML content and extract all hyperlinks (a href attributes). Pay special attention to links in the navigation menu, footer, and main content areas.
25
+ Instructions:
26
+ 1. Parse the HTML content and identify all hyperlinks
27
+ 2. Filter links based on the provided criteria
28
+ 3. Prioritize links from specified areas: #{link_types.join(', ')}
29
+ 4. Return up to #{max_links} most relevant links
30
+ #{if data[:format] == :json
31
+ '5. Format output as a JSON array of URLs'
32
+ else
33
+ '5. Format output as a newline-separated list of URLs'
34
+ end}
17
35
 
18
- - Filter and prioritize the extracted links based on the following criteria:
19
- a. The link must be from the same domain as the current URL.
20
- b. Prioritize links containing keywords such as "features", "products", "solutions", "pricing", "about", "contact", or similar variations.
21
- c. Include social media profile links (e.g., LinkedIn, Instagram, Twitter, Facebook) if available.
22
- d. Exclude links to login pages, search pages, or other utility pages.
36
+ Provide only the links without additional commentary.
37
+ PROMPT
38
+ end
23
39
 
24
- - Select the top 3 most valuable links based on the above criteria.
40
+ # General prompt for content analysis and data extraction
41
+ # Flexible template for various content analysis tasks
42
+ def prompt_analyze_content(data)
43
+ content_type = data[:content_type] || 'webpage content'
44
+ analysis_fields = data[:fields] || %w[summary key_points entities]
45
+ output_format = data[:output_format] || 'structured text'
25
46
 
26
- - Format your output as a JSON array of strings, where each string is a full URL. Use the following format:
27
- <output_format>
28
- ["https://example.com/about-us", "https://example.com/products", "https://example.com/services"]
29
- </output_format>
47
+ <<~PROMPT
48
+ Analyze the provided #{content_type} and extract the requested information.
30
49
 
31
- - The links must be the same domain of following
32
- <domain>
33
- #{data[:current_url]}
34
- </domain>
50
+ Content:
51
+ #{data[:content] || data[:htmls] || data[:text]}
35
52
 
36
- If fewer than 3 relevant links are found, include only the available links in the output array.
53
+ Analysis Fields:
54
+ #{analysis_fields.map { |field| "- #{field}" }.join("\n")}
37
55
 
38
- Remember to use the full URL for each link, including the domain name. If you encounter relative URLs, combine them with the domain from the current URL to create absolute URLs.
56
+ #{"Additional Instructions:\n#{data[:instructions]}" if data[:instructions]}
39
57
 
40
- Provide your final output without any additional explanation or commentary.
58
+ #{if output_format == 'json'
59
+ json_structure = analysis_fields.map { |field| " \"#{field}\": \"value or array\"" }.join(",\n")
60
+ "Output Format: JSON with the following structure:\n{\n#{json_structure}\n}"
61
+ else
62
+ "Output Format: #{output_format}"
63
+ end}
64
+
65
+ #{"Constraints:\n#{data[:constraints]}" if data[:constraints]}
66
+
67
+ Provide a comprehensive analysis focusing on the requested fields.
41
68
  PROMPT
42
69
  end
43
70
 
44
- def prompt_summarize_htmls(data)
71
+ # General prompt for text summarization
72
+ # Applicable to various types of text content
73
+ def prompt_summarize_text(data)
74
+ max_length = data[:max_length] || '200 words'
75
+ focus_areas = data[:focus_areas] || []
76
+ style = data[:style] || 'concise and informative'
77
+
45
78
  <<~PROMPT
46
- Extract useful information from the webpage including a domain, detailed description of what the company does, founding year, country, business model, product description and features, customers and partners, development stage, and social media links. output will be json
47
-
48
- You are tasked with extracting useful information about a company from a given webpage content. Your goal is to analyze the content and extract specific details about the company, its products, and its operations.
49
-
50
- You will be provided with raw HTML content in the following format:
51
-
52
- <html_content>
53
- #{data[:htmls]}
54
- </html_content>
55
-
56
- Carefully read through the webpage content and extract the following information about the company:
57
-
58
- - Name(field name): The company's name
59
- - Domain name(field domain_name): The company's domain
60
- - Description(field description): A comprehensive explanation of what the company does
61
- - Country(field country): The company's country
62
- - Region(field region): The company's region
63
- - Location(field location): The company's location
64
- - Founding on(field founded_on): Which year the company was established
65
- - Business model(field business_model): How the company generates revenue
66
- - Product description(product_description): A brief overview of the company's main product(s) or service(s)
67
- - Product features(product_features): Key features or capabilities of the product(s) or service(s)
68
- - Customers and partners(field customers_and_partners): Notable clients or business partners
69
- - Development stage(field development_stage): The current phase of the company (e.g., startup, growth, established)
70
- - Social media links(field social_media_links): URLs to the company's social media profiles
71
- - instagram_url
72
- - linkedin_url
73
- - twitter_url
74
-
75
- If any of the above information is not available in the webpage content, use "Not available" as the value for that field.
76
-
77
- Present your findings in a JSON format. Here's an example of the expected structure:
78
-
79
- <output_format>
80
- {
81
- "name": "AI-powered customer service",
82
- "domain_name": "example.com",
83
- "description": "XYZ Company develops AI chatbots that help businesses automate customer support...",
84
- "founding_on": 2018,
85
- "country": "United States",
86
- "Region": "SA",
87
- "Location": "SFO",
88
- "business_model": "SaaS subscription",
89
- "product_description": "AI-powered chatbot platform for customer service automation",
90
- "product_features": ["Natural language processing", "Multi-language support", "Integration with CRM systems"],
91
- "customers_and_partners": ["ABC Corp", "123 Industries", "Big Tech Co."],
92
- "development_stage": "Growth",
93
- "social_media_links": {
94
- "linkedin_url": "https://www.linkedin.com/company/xyzcompany",
95
- "twitter_url": "https://twitter.com/xyzcompany",
96
- "instagram_url": "https://www.instagram.com/xyzcompany"
97
- }
98
- }
99
- </output_format>
100
-
101
- Remember to use only the information provided in the webpage content. Do not include any external information or make assumptions beyond what is explicitly stated or strongly implied in the given content.
102
-
103
- Present your final output in JSON format, enclosed within <json_output> tags.
79
+ Summarize the following text content.
80
+
81
+ Text:
82
+ #{data[:text] || data[:content] || data[:description]}
83
+
84
+ Summary Requirements:
85
+ - Maximum Length: #{max_length}
86
+ - Style: #{style}
87
+ #{"- Focus Areas: #{focus_areas.join(', ')}" if focus_areas.any?}
88
+ #{"- Target Audience: #{data[:audience]}" if data[:audience]}
89
+
90
+ #{'Include key points and main themes.' if data[:include_key_points]}
91
+
92
+ #{if data[:output_format] == 'bullet_points'
93
+ 'Format as bullet points.'
94
+ elsif data[:output_format] == 'paragraph'
95
+ 'Format as a single paragraph.'
96
+ end}
97
+
98
+ Provide a clear and accurate summary.
104
99
  PROMPT
105
100
  end
106
101
 
107
- def prompt_summarize_description(data)
102
+ # General prompt for data classification and categorization
103
+ # Useful for various classification tasks
104
+ def prompt_classify_content(data)
105
+ categories = data[:categories] || []
106
+ classification_type = data[:classification_type] || 'content'
107
+ confidence_scores = data[:include_confidence] || false
108
+
108
109
  <<~PROMPT
109
- Given the company's name, domain, description, and a list of industry-related keywords,
110
- please summarize the company's core business and identify the three most relevant industries.
111
- Highlight the company's unique value proposition, its primary market focus,
112
- and any distinguishing features that set it apart within the identified industries.
113
- Be as objective as possible.
114
-
115
- Name: #{data[:name]}
116
- Domain Name: #{data[:domain_name]}
117
- Industry: #{data[:industries]}
118
- Description: #{data[:description]}
110
+ Classify the provided #{classification_type} into the most appropriate category.
111
+
112
+ Content to Classify:
113
+ #{data[:content] || data[:text] || data[:description]}
114
+
115
+ Available Categories:
116
+ #{categories.map.with_index(1) { |cat, i| "#{i}. #{cat}" }.join("\n")}
117
+
118
+ #{"Classification Criteria:\n#{data[:classification_criteria]}" if data[:classification_criteria]}
119
+
120
+ #{if confidence_scores
121
+ 'Output Format: JSON with category and confidence score (0-1)'
122
+ else
123
+ 'Output Format: Return the most appropriate category name'
124
+ end}
125
+
126
+ #{if data[:multiple_categories]
127
+ "Note: Multiple categories may apply - select up to #{data[:max_categories] || 3} most relevant."
128
+ else
129
+ 'Note: Select only the single most appropriate category.'
130
+ end}
131
+
132
+ Provide your classification based on the content analysis.
119
133
  PROMPT
120
134
  end
121
135
 
136
+ # Flexible custom prompt template
137
+ # Allows for dynamic prompt creation with variable substitution
122
138
  def prompt_custom(data)
123
139
  template = data.fetch(:template)
124
140
  template % data
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LlmConductor
4
- VERSION = '0.1.1'
4
+ VERSION = '1.0.1'
5
5
  end
data/lib/llm_conductor.rb CHANGED
@@ -74,9 +74,10 @@ module LlmConductor
74
74
 
75
75
  # List of supported prompt types
76
76
  SUPPORTED_PROMPT_TYPES = %i[
77
- featured_links
78
- summarize_htmls
79
- summarize_description
77
+ extract_links
78
+ analyze_content
79
+ summarize_text
80
+ classify_content
80
81
  custom
81
82
  ].freeze
82
83
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: llm_conductor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Zheng
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-09-25 00:00:00.000000000 Z
10
+ date: 2025-10-01 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activesupport