llm_conductor 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/.DS_Store +0 -0
- data/.rspec +4 -0
- data/.rubocop.yml +103 -0
- data/.rubocop_todo.yml +54 -0
- data/.ruby-version +1 -0
- data/README.md +413 -0
- data/Rakefile +12 -0
- data/config/initializers/llm_conductor.rb +27 -0
- data/examples/data_builder_usage.rb +301 -0
- data/examples/prompt_registration.rb +133 -0
- data/examples/rag_usage.rb +108 -0
- data/examples/simple_usage.rb +48 -0
- data/lib/llm_conductor/client_factory.rb +33 -0
- data/lib/llm_conductor/clients/base_client.rb +98 -0
- data/lib/llm_conductor/clients/gpt_client.rb +24 -0
- data/lib/llm_conductor/clients/ollama_client.rb +24 -0
- data/lib/llm_conductor/clients/openrouter_client.rb +30 -0
- data/lib/llm_conductor/configuration.rb +97 -0
- data/lib/llm_conductor/data_builder.rb +211 -0
- data/lib/llm_conductor/prompt_manager.rb +72 -0
- data/lib/llm_conductor/prompts/base_prompt.rb +90 -0
- data/lib/llm_conductor/prompts.rb +127 -0
- data/lib/llm_conductor/response.rb +86 -0
- data/lib/llm_conductor/version.rb +5 -0
- data/lib/llm_conductor.rb +76 -0
- data/sig/llm_conductor.rbs +4 -0
- metadata +157 -0
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LlmConductor
|
4
|
+
# Collection of pre-built prompt templates for common LLM tasks including
|
5
|
+
# content analysis, link extraction, and data summarization.
|
6
|
+
module Prompts
|
7
|
+
def prompt_featured_links(data)
|
8
|
+
<<~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:
|
10
|
+
|
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>
|
15
|
+
|
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.
|
17
|
+
|
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.
|
23
|
+
|
24
|
+
- Select the top 3 most valuable links based on the above criteria.
|
25
|
+
|
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>
|
30
|
+
|
31
|
+
- The links must be the same domain of following
|
32
|
+
<domain>
|
33
|
+
#{data[:current_url]}
|
34
|
+
</domain>
|
35
|
+
|
36
|
+
If fewer than 3 relevant links are found, include only the available links in the output array.
|
37
|
+
|
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.
|
39
|
+
|
40
|
+
Provide your final output without any additional explanation or commentary.
|
41
|
+
PROMPT
|
42
|
+
end
|
43
|
+
|
44
|
+
def prompt_summarize_htmls(data)
|
45
|
+
<<~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.
|
104
|
+
PROMPT
|
105
|
+
end
|
106
|
+
|
107
|
+
def prompt_summarize_description(data)
|
108
|
+
<<~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]}
|
119
|
+
PROMPT
|
120
|
+
end
|
121
|
+
|
122
|
+
def prompt_custom(data)
|
123
|
+
template = data.fetch(:template)
|
124
|
+
template % data
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LlmConductor
|
4
|
+
# Response object that encapsulates the result of LLM generation
|
5
|
+
# with metadata like token usage and cost information
|
6
|
+
class Response
|
7
|
+
attr_reader :output, :input_tokens, :output_tokens, :metadata, :model
|
8
|
+
|
9
|
+
def initialize(output:, model:, input_tokens: nil, output_tokens: nil, metadata: {})
|
10
|
+
@output = output
|
11
|
+
@model = model
|
12
|
+
@input_tokens = input_tokens
|
13
|
+
@output_tokens = output_tokens
|
14
|
+
@metadata = metadata || {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def total_tokens
|
18
|
+
(@input_tokens || 0) + (@output_tokens || 0)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Calculate estimated cost based on model and token usage
|
22
|
+
def estimated_cost
|
23
|
+
return nil unless valid_for_cost_calculation?
|
24
|
+
|
25
|
+
pricing = model_pricing
|
26
|
+
return nil unless pricing
|
27
|
+
|
28
|
+
calculate_cost(pricing[:input_rate], pricing[:output_rate])
|
29
|
+
end
|
30
|
+
|
31
|
+
# Check if the response was successful
|
32
|
+
def success?
|
33
|
+
!@output.nil? && !@output.empty? && @metadata[:error].nil?
|
34
|
+
end
|
35
|
+
|
36
|
+
# Get metadata with cost included if available
|
37
|
+
def metadata_with_cost
|
38
|
+
cost = estimated_cost
|
39
|
+
cost ? @metadata.merge(cost:) : @metadata
|
40
|
+
end
|
41
|
+
|
42
|
+
# Parse JSON from the output
|
43
|
+
def parse_json
|
44
|
+
return nil unless success? && @output
|
45
|
+
|
46
|
+
JSON.parse(@output.strip)
|
47
|
+
rescue JSON::ParserError => e
|
48
|
+
raise JSON::ParserError, "Failed to parse JSON response: #{e.message}"
|
49
|
+
end
|
50
|
+
|
51
|
+
# Extract text between code blocks
|
52
|
+
def extract_code_block(language = nil)
|
53
|
+
return nil unless @output
|
54
|
+
|
55
|
+
pattern = if language
|
56
|
+
/```#{Regexp.escape(language)}\s*(.*?)```/m
|
57
|
+
else
|
58
|
+
/```(?:\w*)\s*(.*?)```/m
|
59
|
+
end
|
60
|
+
|
61
|
+
match = @output.match(pattern)
|
62
|
+
match ? match[1].strip : nil
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def valid_for_cost_calculation?
|
68
|
+
@model && total_tokens.positive?
|
69
|
+
end
|
70
|
+
|
71
|
+
def model_pricing
|
72
|
+
case @model
|
73
|
+
when /gpt-3\.5-turbo/
|
74
|
+
{ input_rate: 0.0000015, output_rate: 0.000002 }
|
75
|
+
when /gpt-4o-mini/
|
76
|
+
{ input_rate: 0.000000150, output_rate: 0.0000006 }
|
77
|
+
when /gpt-4/
|
78
|
+
{ input_rate: 0.00003, output_rate: 0.00006 }
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def calculate_cost(input_rate, output_rate)
|
83
|
+
(@input_tokens || 0) * input_rate + (@output_tokens || 0) * output_rate
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'llm_conductor/version'
|
4
|
+
require_relative 'llm_conductor/configuration'
|
5
|
+
require_relative 'llm_conductor/response'
|
6
|
+
require_relative 'llm_conductor/data_builder'
|
7
|
+
require_relative 'llm_conductor/prompts'
|
8
|
+
require_relative 'llm_conductor/prompts/base_prompt'
|
9
|
+
require_relative 'llm_conductor/prompt_manager'
|
10
|
+
require_relative 'llm_conductor/clients/base_client'
|
11
|
+
require_relative 'llm_conductor/clients/gpt_client'
|
12
|
+
require_relative 'llm_conductor/clients/ollama_client'
|
13
|
+
require_relative 'llm_conductor/clients/openrouter_client'
|
14
|
+
require_relative 'llm_conductor/client_factory'
|
15
|
+
|
16
|
+
# LLM Conductor provides a unified interface for multiple Language Model providers
|
17
|
+
# including OpenAI GPT, OpenRouter, and Ollama with built-in prompt templates,
|
18
|
+
# token counting, and extensible client architecture.
|
19
|
+
module LlmConductor
|
20
|
+
class Error < StandardError; end
|
21
|
+
|
22
|
+
# Main entry point for creating LLM clients
|
23
|
+
def self.build_client(model:, type:, vendor: nil)
|
24
|
+
ClientFactory.build(model:, type:, vendor:)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Unified generate method supporting both simple prompts and legacy template-based generation
|
28
|
+
def self.generate(model: nil, prompt: nil, type: nil, data: nil, vendor: nil)
|
29
|
+
if prompt && !type && !data
|
30
|
+
generate_simple_prompt(model:, prompt:, vendor:)
|
31
|
+
elsif type && data && !prompt
|
32
|
+
generate_with_template(model:, type:, data:, vendor:)
|
33
|
+
else
|
34
|
+
raise ArgumentError,
|
35
|
+
"Invalid arguments. Use either: generate(prompt: 'text') or generate(type: :custom, data: {...})"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class << self
|
40
|
+
private
|
41
|
+
|
42
|
+
def generate_simple_prompt(model:, prompt:, vendor:)
|
43
|
+
model ||= configuration.default_model
|
44
|
+
vendor ||= ClientFactory.determine_vendor(model)
|
45
|
+
client_class = client_class_for_vendor(vendor)
|
46
|
+
client = client_class.new(model:, type: :direct)
|
47
|
+
client.generate_simple(prompt:)
|
48
|
+
end
|
49
|
+
|
50
|
+
def generate_with_template(model:, type:, data:, vendor:)
|
51
|
+
client = build_client(model:, type:, vendor:)
|
52
|
+
client.generate(data:)
|
53
|
+
end
|
54
|
+
|
55
|
+
def client_class_for_vendor(vendor)
|
56
|
+
case vendor
|
57
|
+
when :openai, :gpt then Clients::GptClient
|
58
|
+
when :openrouter then Clients::OpenrouterClient
|
59
|
+
when :ollama then Clients::OllamaClient
|
60
|
+
else
|
61
|
+
raise ArgumentError, "Unsupported vendor: #{vendor}. Supported vendors: openai, openrouter, ollama"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# List of supported vendors
|
67
|
+
SUPPORTED_VENDORS = %i[openai openrouter ollama].freeze
|
68
|
+
|
69
|
+
# List of supported prompt types
|
70
|
+
SUPPORTED_PROMPT_TYPES = %i[
|
71
|
+
featured_links
|
72
|
+
summarize_htmls
|
73
|
+
summarize_description
|
74
|
+
custom
|
75
|
+
].freeze
|
76
|
+
end
|
metadata
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: llm_conductor
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ben Zheng
|
8
|
+
bindir: exe
|
9
|
+
cert_chain: []
|
10
|
+
date: 2025-09-19 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: activesupport
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - ">="
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '6.0'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ">="
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '6.0'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: ollama-ai
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '1.3'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '1.3'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: ruby-openai
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '7.0'
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '7.0'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: tiktoken_ruby
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: 0.0.7
|
61
|
+
type: :runtime
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: 0.0.7
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: rubocop-performance
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '1.19'
|
75
|
+
type: :development
|
76
|
+
prerelease: false
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '1.19'
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: rubocop-rspec
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '3.0'
|
89
|
+
type: :development
|
90
|
+
prerelease: false
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "~>"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '3.0'
|
96
|
+
description: LLM Conductor provides a clean, unified interface for working with multiple
|
97
|
+
Language Model providers including OpenAI GPT, OpenRouter, and Ollama. Features
|
98
|
+
include prompt templating, token counting, and extensible client architecture.
|
99
|
+
email:
|
100
|
+
- ben@ekohe.com
|
101
|
+
executables: []
|
102
|
+
extensions: []
|
103
|
+
extra_rdoc_files: []
|
104
|
+
files:
|
105
|
+
- ".DS_Store"
|
106
|
+
- ".rspec"
|
107
|
+
- ".rubocop.yml"
|
108
|
+
- ".rubocop_todo.yml"
|
109
|
+
- ".ruby-version"
|
110
|
+
- README.md
|
111
|
+
- Rakefile
|
112
|
+
- config/initializers/llm_conductor.rb
|
113
|
+
- examples/data_builder_usage.rb
|
114
|
+
- examples/prompt_registration.rb
|
115
|
+
- examples/rag_usage.rb
|
116
|
+
- examples/simple_usage.rb
|
117
|
+
- lib/llm_conductor.rb
|
118
|
+
- lib/llm_conductor/client_factory.rb
|
119
|
+
- lib/llm_conductor/clients/base_client.rb
|
120
|
+
- lib/llm_conductor/clients/gpt_client.rb
|
121
|
+
- lib/llm_conductor/clients/ollama_client.rb
|
122
|
+
- lib/llm_conductor/clients/openrouter_client.rb
|
123
|
+
- lib/llm_conductor/configuration.rb
|
124
|
+
- lib/llm_conductor/data_builder.rb
|
125
|
+
- lib/llm_conductor/prompt_manager.rb
|
126
|
+
- lib/llm_conductor/prompts.rb
|
127
|
+
- lib/llm_conductor/prompts/base_prompt.rb
|
128
|
+
- lib/llm_conductor/response.rb
|
129
|
+
- lib/llm_conductor/version.rb
|
130
|
+
- sig/llm_conductor.rbs
|
131
|
+
homepage: https://github.com/ekohe/llm_conductor
|
132
|
+
licenses: []
|
133
|
+
metadata:
|
134
|
+
allowed_push_host: https://rubygems.org
|
135
|
+
homepage_uri: https://github.com/ekohe/llm_conductor
|
136
|
+
source_code_uri: https://github.com/ekohe/llm_conductor
|
137
|
+
changelog_uri: https://github.com/ekohe/llm_conductor/blob/main/CHANGELOG.md
|
138
|
+
rubygems_mfa_required: 'true'
|
139
|
+
rdoc_options: []
|
140
|
+
require_paths:
|
141
|
+
- lib
|
142
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
143
|
+
requirements:
|
144
|
+
- - ">="
|
145
|
+
- !ruby/object:Gem::Version
|
146
|
+
version: 3.1.0
|
147
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
148
|
+
requirements:
|
149
|
+
- - ">="
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '0'
|
152
|
+
requirements: []
|
153
|
+
rubygems_version: 3.6.2
|
154
|
+
specification_version: 4
|
155
|
+
summary: A flexible Ruby gem for orchestrating multiple LLM providers with unified
|
156
|
+
interface
|
157
|
+
test_files: []
|