langchainrb 0.7.5 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +78 -0
  3. data/README.md +113 -56
  4. data/lib/langchain/assistants/assistant.rb +213 -0
  5. data/lib/langchain/assistants/message.rb +58 -0
  6. data/lib/langchain/assistants/thread.rb +34 -0
  7. data/lib/langchain/chunker/markdown.rb +37 -0
  8. data/lib/langchain/chunker/recursive_text.rb +0 -2
  9. data/lib/langchain/chunker/semantic.rb +1 -3
  10. data/lib/langchain/chunker/sentence.rb +0 -2
  11. data/lib/langchain/chunker/text.rb +0 -2
  12. data/lib/langchain/contextual_logger.rb +1 -1
  13. data/lib/langchain/data.rb +4 -3
  14. data/lib/langchain/llm/ai21.rb +1 -1
  15. data/lib/langchain/llm/anthropic.rb +86 -11
  16. data/lib/langchain/llm/aws_bedrock.rb +52 -0
  17. data/lib/langchain/llm/azure.rb +10 -97
  18. data/lib/langchain/llm/base.rb +3 -2
  19. data/lib/langchain/llm/cohere.rb +5 -7
  20. data/lib/langchain/llm/google_palm.rb +4 -2
  21. data/lib/langchain/llm/google_vertex_ai.rb +151 -0
  22. data/lib/langchain/llm/hugging_face.rb +1 -1
  23. data/lib/langchain/llm/llama_cpp.rb +18 -16
  24. data/lib/langchain/llm/mistral_ai.rb +68 -0
  25. data/lib/langchain/llm/ollama.rb +209 -27
  26. data/lib/langchain/llm/openai.rb +138 -170
  27. data/lib/langchain/llm/prompts/ollama/summarize_template.yaml +9 -0
  28. data/lib/langchain/llm/replicate.rb +1 -7
  29. data/lib/langchain/llm/response/anthropic_response.rb +20 -0
  30. data/lib/langchain/llm/response/base_response.rb +7 -0
  31. data/lib/langchain/llm/response/google_palm_response.rb +4 -0
  32. data/lib/langchain/llm/response/google_vertex_ai_response.rb +33 -0
  33. data/lib/langchain/llm/response/llama_cpp_response.rb +13 -0
  34. data/lib/langchain/llm/response/mistral_ai_response.rb +39 -0
  35. data/lib/langchain/llm/response/ollama_response.rb +27 -1
  36. data/lib/langchain/llm/response/openai_response.rb +8 -0
  37. data/lib/langchain/loader.rb +3 -2
  38. data/lib/langchain/output_parsers/base.rb +0 -4
  39. data/lib/langchain/output_parsers/output_fixing_parser.rb +7 -14
  40. data/lib/langchain/output_parsers/structured_output_parser.rb +0 -10
  41. data/lib/langchain/processors/csv.rb +37 -3
  42. data/lib/langchain/processors/eml.rb +64 -0
  43. data/lib/langchain/processors/markdown.rb +17 -0
  44. data/lib/langchain/processors/pptx.rb +29 -0
  45. data/lib/langchain/prompt/loading.rb +1 -1
  46. data/lib/langchain/tool/base.rb +21 -53
  47. data/lib/langchain/tool/calculator/calculator.json +19 -0
  48. data/lib/langchain/tool/{calculator.rb → calculator/calculator.rb} +8 -16
  49. data/lib/langchain/tool/database/database.json +46 -0
  50. data/lib/langchain/tool/database/database.rb +99 -0
  51. data/lib/langchain/tool/file_system/file_system.json +57 -0
  52. data/lib/langchain/tool/file_system/file_system.rb +32 -0
  53. data/lib/langchain/tool/google_search/google_search.json +19 -0
  54. data/lib/langchain/tool/{google_search.rb → google_search/google_search.rb} +5 -15
  55. data/lib/langchain/tool/ruby_code_interpreter/ruby_code_interpreter.json +19 -0
  56. data/lib/langchain/tool/{ruby_code_interpreter.rb → ruby_code_interpreter/ruby_code_interpreter.rb} +8 -4
  57. data/lib/langchain/tool/vectorsearch/vectorsearch.json +24 -0
  58. data/lib/langchain/tool/vectorsearch/vectorsearch.rb +36 -0
  59. data/lib/langchain/tool/weather/weather.json +19 -0
  60. data/lib/langchain/tool/{weather.rb → weather/weather.rb} +3 -15
  61. data/lib/langchain/tool/wikipedia/wikipedia.json +19 -0
  62. data/lib/langchain/tool/{wikipedia.rb → wikipedia/wikipedia.rb} +9 -9
  63. data/lib/langchain/utils/token_length/ai21_validator.rb +6 -2
  64. data/lib/langchain/utils/token_length/base_validator.rb +1 -1
  65. data/lib/langchain/utils/token_length/cohere_validator.rb +6 -2
  66. data/lib/langchain/utils/token_length/google_palm_validator.rb +5 -1
  67. data/lib/langchain/utils/token_length/openai_validator.rb +55 -1
  68. data/lib/langchain/utils/token_length/token_limit_exceeded.rb +1 -1
  69. data/lib/langchain/vectorsearch/base.rb +11 -4
  70. data/lib/langchain/vectorsearch/chroma.rb +10 -1
  71. data/lib/langchain/vectorsearch/elasticsearch.rb +53 -4
  72. data/lib/langchain/vectorsearch/epsilla.rb +149 -0
  73. data/lib/langchain/vectorsearch/hnswlib.rb +5 -1
  74. data/lib/langchain/vectorsearch/milvus.rb +4 -2
  75. data/lib/langchain/vectorsearch/pgvector.rb +14 -4
  76. data/lib/langchain/vectorsearch/pinecone.rb +8 -5
  77. data/lib/langchain/vectorsearch/qdrant.rb +16 -4
  78. data/lib/langchain/vectorsearch/weaviate.rb +20 -2
  79. data/lib/langchain/version.rb +1 -1
  80. data/lib/langchain.rb +20 -5
  81. metadata +182 -45
  82. data/lib/langchain/agent/agents.md +0 -54
  83. data/lib/langchain/agent/base.rb +0 -20
  84. data/lib/langchain/agent/react_agent/react_agent_prompt.yaml +0 -26
  85. data/lib/langchain/agent/react_agent.rb +0 -131
  86. data/lib/langchain/agent/sql_query_agent/sql_query_agent_answer_prompt.yaml +0 -11
  87. data/lib/langchain/agent/sql_query_agent/sql_query_agent_sql_prompt.yaml +0 -21
  88. data/lib/langchain/agent/sql_query_agent.rb +0 -82
  89. data/lib/langchain/conversation/context.rb +0 -8
  90. data/lib/langchain/conversation/memory.rb +0 -86
  91. data/lib/langchain/conversation/message.rb +0 -48
  92. data/lib/langchain/conversation/prompt.rb +0 -8
  93. data/lib/langchain/conversation/response.rb +0 -8
  94. data/lib/langchain/conversation.rb +0 -93
  95. data/lib/langchain/tool/database.rb +0 -90
@@ -6,13 +6,11 @@ module Langchain::OutputParsers
6
6
  class OutputFixingParser < Base
7
7
  attr_reader :llm, :parser, :prompt
8
8
 
9
- #
10
9
  # Initializes a new instance of the class.
11
10
  #
12
11
  # @param llm [Langchain::LLM] The LLM used in the fixing process
13
12
  # @param parser [Langchain::OutputParsers] The parser originally used which resulted in parsing error
14
13
  # @param prompt [Langchain::Prompt::PromptTemplate]
15
- #
16
14
  def initialize(llm:, parser:, prompt:)
17
15
  raise ArgumentError.new("llm must be an instance of Langchain::LLM got: #{llm.class}") unless llm.is_a?(Langchain::LLM::Base)
18
16
  raise ArgumentError.new("parser must be an instance of Langchain::OutputParsers got #{parser.class}") unless parser.is_a?(Langchain::OutputParsers::Base)
@@ -30,17 +28,14 @@ module Langchain::OutputParsers
30
28
  }
31
29
  end
32
30
 
33
- #
34
31
  # calls get_format_instructions on the @parser
35
32
  #
36
33
  # @return [String] Instructions for how the output of a language model should be formatted
37
34
  # according to the @schema.
38
- #
39
35
  def get_format_instructions
40
36
  parser.get_format_instructions
41
37
  end
42
38
 
43
- #
44
39
  # Parse the output of an LLM call, if fails with OutputParserException
45
40
  # then call the LLM with a fix prompt in an attempt to get the correctly
46
41
  # formatted response
@@ -48,21 +43,20 @@ module Langchain::OutputParsers
48
43
  # @param completion [String] Text output from the LLM call
49
44
  #
50
45
  # @return [Object] object that is succesfully parsed by @parser.parse
51
- #
52
46
  def parse(completion)
53
47
  parser.parse(completion)
54
48
  rescue OutputParserException => e
55
49
  new_completion = llm.chat(
56
- prompt: prompt.format(
57
- instructions: parser.get_format_instructions,
58
- completion: completion,
59
- error: e
60
- )
61
- )
50
+ messages: [{role: "user",
51
+ content: prompt.format(
52
+ instructions: parser.get_format_instructions,
53
+ completion: completion,
54
+ error: e
55
+ )}]
56
+ ).completion
62
57
  parser.parse(new_completion)
63
58
  end
64
59
 
65
- #
66
60
  # Creates a new instance of the class using the given JSON::Schema.
67
61
  #
68
62
  # @param llm [Langchain::LLM] The LLM used in the fixing process
@@ -70,7 +64,6 @@ module Langchain::OutputParsers
70
64
  # @param prompt [Langchain::Prompt::PromptTemplate]
71
65
  #
72
66
  # @return [Object] A new instance of the class
73
- #
74
67
  def self.from_llm(llm:, parser:, prompt: nil)
75
68
  new(llm: llm, parser: parser, prompt: prompt || naive_fix_prompt)
76
69
  end
@@ -5,15 +5,12 @@ require "json-schema"
5
5
 
6
6
  module Langchain::OutputParsers
7
7
  # = Structured Output Parser
8
- #
9
8
  class StructuredOutputParser < Base
10
9
  attr_reader :schema
11
10
 
12
- #
13
11
  # Initializes a new instance of the class.
14
12
  #
15
13
  # @param schema [JSON::Schema] The json schema
16
- #
17
14
  def initialize(schema:)
18
15
  @schema = validate_schema!(schema)
19
16
  end
@@ -25,24 +22,20 @@ module Langchain::OutputParsers
25
22
  }
26
23
  end
27
24
 
28
- #
29
25
  # Creates a new instance of the class using the given JSON::Schema.
30
26
  #
31
27
  # @param schema [JSON::Schema] The JSON::Schema to use
32
28
  #
33
29
  # @return [Object] A new instance of the class
34
- #
35
30
  def self.from_json_schema(schema)
36
31
  new(schema: schema)
37
32
  end
38
33
 
39
- #
40
34
  # Returns a string containing instructions for how the output of a language model should be formatted
41
35
  # according to the @schema.
42
36
  #
43
37
  # @return [String] Instructions for how the output of a language model should be formatted
44
38
  # according to the @schema.
45
- #
46
39
  def get_format_instructions
47
40
  <<~INSTRUCTIONS
48
41
  You must format your output as a JSON value that adheres to a given "JSON Schema" instance.
@@ -62,13 +55,10 @@ module Langchain::OutputParsers
62
55
  INSTRUCTIONS
63
56
  end
64
57
 
65
- #
66
58
  # Parse the output of an LLM call extracting an object that abides by the @schema
67
59
  #
68
60
  # @param text [String] Text output from the LLM call
69
- #
70
61
  # @return [Object] object that abides by the @schema
71
- #
72
62
  def parse(text)
73
63
  json = text.include?("```") ? text.strip.split(/```(?:json)?/)[1] : text.strip
74
64
  parsed = JSON.parse(json)
@@ -5,15 +5,26 @@ require "csv"
5
5
  module Langchain
6
6
  module Processors
7
7
  class CSV < Base
8
+ class InvalidChunkMode < StandardError; end
9
+
8
10
  EXTENSIONS = [".csv"]
9
11
  CONTENT_TYPES = ["text/csv"]
12
+ CHUNK_MODE = {
13
+ row: "row",
14
+ file: "file"
15
+ }
10
16
 
11
17
  # Parse the document and return the text
12
18
  # @param [File] data
13
- # @return [Array of Hash]
19
+ # @return [String]
14
20
  def parse(data)
15
- ::CSV.new(data.read, col_sep: separator).map do |row|
16
- row.map(&:strip)
21
+ case chunk_mode
22
+ when CHUNK_MODE[:row]
23
+ chunk_row(data)
24
+ when CHUNK_MODE[:file]
25
+ chunk_file(data)
26
+ else
27
+ raise InvalidChunkMode
17
28
  end
18
29
  end
19
30
 
@@ -22,6 +33,29 @@ module Langchain
22
33
  def separator
23
34
  @options[:col_sep] || ","
24
35
  end
36
+
37
+ def chunk_mode
38
+ if @options[:chunk_mode].to_s.empty?
39
+ CHUNK_MODE[:row]
40
+ else
41
+ raise InvalidChunkMode unless CHUNK_MODE.value?(@options[:chunk_mode])
42
+
43
+ @options[:chunk_mode]
44
+ end
45
+ end
46
+
47
+ def chunk_row(data)
48
+ ::CSV.new(data.read, col_sep: separator).map do |row|
49
+ row
50
+ .compact
51
+ .map(&:strip)
52
+ .join(separator)
53
+ end.join("\n\n")
54
+ end
55
+
56
+ def chunk_file(data)
57
+ data.read
58
+ end
25
59
  end
26
60
  end
27
61
  end
@@ -0,0 +1,64 @@
1
+ require "uri"
2
+
3
+ module Langchain
4
+ module Processors
5
+ class Eml < Base
6
+ EXTENSIONS = [".eml"]
7
+ CONTENT_TYPES = ["message/rfc822"]
8
+
9
+ def initialize(*)
10
+ depends_on "mail"
11
+ end
12
+
13
+ # Parse the document and return the cleaned text
14
+ # @param [File] data
15
+ # @return [String]
16
+ def parse(data)
17
+ mail = Mail.read(data.path)
18
+ text_content = extract_text_content(mail)
19
+ clean_content(text_content)
20
+ end
21
+
22
+ private
23
+
24
+ # Extract text content from the email, preferring plaintext over HTML
25
+ def extract_text_content(mail)
26
+ text_content = ""
27
+ text_content += "From: #{mail.from}\n" \
28
+ "To: #{mail.to}\n" \
29
+ "Cc: #{mail.cc}\n" \
30
+ "Bcc: #{mail.bcc}\n" \
31
+ "Subject: #{mail.subject}\n" \
32
+ "Date: #{mail.date}\n\n"
33
+ if mail.multipart?
34
+ mail.parts.each do |part|
35
+ if part.content_type.start_with?("text/plain")
36
+ text_content += part.body.decoded.force_encoding("UTF-8").strip + "\n"
37
+ elsif part.content_type.start_with?("multipart/alternative", "multipart/mixed")
38
+ text_content += extract_text_content(part) + "\n" # Recursively extract from multipart
39
+ elsif part.content_type.start_with?("message/rfc822")
40
+ # Handle embedded .eml parts as separate emails
41
+ embedded_mail = Mail.read_from_string(part.body.decoded)
42
+ text_content += "--- Begin Embedded Email ---\n"
43
+ text_content += extract_text_content(embedded_mail) + "\n"
44
+ text_content += "--- End Embedded Email ---\n"
45
+ end
46
+ end
47
+ elsif mail.content_type.start_with?("text/plain")
48
+ text_content = mail.body.decoded.force_encoding("UTF-8").strip
49
+ end
50
+ text_content
51
+ end
52
+
53
+ # Clean and format the extracted content
54
+ def clean_content(content)
55
+ content
56
+ .gsub(/\[cid:[^\]]+\]/, "") # Remove embedded image references
57
+ .gsub(URI::DEFAULT_PARSER.make_regexp(%w[http https])) { |match| "<#{match}>" } # Format URLs
58
+ .gsub(/\r\n?/, "\n") # Normalize line endings to Unix style
59
+ .gsub(/[\u200B-\u200D\uFEFF]/, "") # Remove zero width spaces and similar characters
60
+ .gsub(/<\/?[^>]+>/, "") # Remove any HTML tags that might have sneaked in
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Langchain
4
+ module Processors
5
+ class Markdown < Base
6
+ EXTENSIONS = [".markdown", ".md"]
7
+ CONTENT_TYPES = ["text/markdown"]
8
+
9
+ # Parse the document and return the text
10
+ # @param [File] data
11
+ # @return [String]
12
+ def parse(data)
13
+ data.read
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Langchain
4
+ module Processors
5
+ class Pptx < Base
6
+ EXTENSIONS = [".pptx"]
7
+ CONTENT_TYPES = ["application/vnd.openxmlformats-officedocument.presentationml.presentation"]
8
+
9
+ def initialize(*)
10
+ depends_on "power_point_pptx"
11
+ end
12
+
13
+ # Parse the document and return the text
14
+ # @param [File] data
15
+ # @return [String]
16
+ def parse(data)
17
+ presentation = PowerPointPptx::Document.open(data)
18
+
19
+ slides = presentation.slides
20
+ contents = slides.map(&:content)
21
+ text = contents.map do |sections|
22
+ sections.map(&:strip).join(" ")
23
+ end
24
+
25
+ text.join("\n\n")
26
+ end
27
+ end
28
+ end
29
+ end
@@ -33,7 +33,7 @@ module Langchain::Prompt
33
33
  when ".json"
34
34
  config = JSON.parse(File.read(file_path))
35
35
  when ".yaml", ".yml"
36
- config = YAML.safe_load(File.read(file_path))
36
+ config = YAML.safe_load_file(file_path)
37
37
  else
38
38
  raise ArgumentError, "Got unsupported file type #{file_path.extname}"
39
39
  end
@@ -9,6 +9,7 @@ module Langchain::Tool
9
9
  #
10
10
  # - {Langchain::Tool::Calculator}: calculate the result of a math expression
11
11
  # - {Langchain::Tool::Database}: executes SQL queries
12
+ # - {Langchain::Tool::FileSystem}: interacts with files
12
13
  # - {Langchain::Tool::GoogleSearch}: search on Google (via SerpAPI)
13
14
  # - {Langchain::Tool::RubyCodeInterpreter}: runs ruby code
14
15
  # - {Langchain::Tool::Weather}: gets current weather data
@@ -29,8 +30,9 @@ module Langchain::Tool
29
30
  #
30
31
  # 3. Pass the tools when Agent is instantiated.
31
32
  #
32
- # agent = Langchain::Agent::ReActAgent.new(
33
- # llm: Langchain::LLM::OpenAI.new(api_key: "YOUR_API_KEY"), # or other like Cohere, Hugging Face, Google Palm or Replicate
33
+ # agent = Langchain::Assistant.new(
34
+ # llm: Langchain::LLM::OpenAI.new(api_key: "YOUR_API_KEY"), # or other LLM that supports function calling (coming soon)
35
+ # thread: Langchain::Thread.new,
34
36
  # tools: [
35
37
  # Langchain::Tool::GoogleSearch.new(api_key: "YOUR_API_KEY"),
36
38
  # Langchain::Tool::Calculator.new,
@@ -42,17 +44,16 @@ module Langchain::Tool
42
44
  #
43
45
  # 1. Create a new file in lib/langchain/tool/your_tool_name.rb
44
46
  # 2. Create a class in the file that inherits from {Langchain::Tool::Base}
45
- # 3. Add `NAME=` and `DESCRIPTION=` constants in your Tool class
46
- # 4. Implement `execute(input:)` method in your tool class
47
- # 5. Add your tool to the {file:README.md}
47
+ # 3. Add `NAME=` and `ANNOTATIONS_PATH=` constants in your Tool class
48
+ # 4. Implement various methods in your tool class
49
+ # 5. Create a sidecar .json file in the same directory as your tool file annotating the methods in the Open API format
50
+ # 6. Add your tool to the {file:README.md}
48
51
  class Base
49
52
  include Langchain::DependencyHelper
50
53
 
51
- #
52
54
  # Returns the NAME constant of the tool
53
55
  #
54
56
  # @return [String] tool name
55
- #
56
57
  def name
57
58
  self.class.const_get(:NAME)
58
59
  end
@@ -63,55 +64,22 @@ module Langchain::Tool
63
64
  }
64
65
  end
65
66
 
67
+ # Returns the tool as a list of OpenAI formatted functions
66
68
  #
67
- # Returns the DESCRIPTION constant of the tool
68
- #
69
- # @return [String] tool description
70
- #
71
- def description
72
- self.class.const_get(:DESCRIPTION)
69
+ # @return [Hash] tool as an OpenAI tool
70
+ def to_openai_tools
71
+ method_annotations
73
72
  end
74
73
 
75
- #
76
- # Sets the DESCRIPTION constant of the tool
77
- #
78
- # @param value [String] tool description
79
- #
80
- def self.description(value)
81
- const_set(:DESCRIPTION, value.tr("\n", " ").strip)
82
- end
83
-
84
- #
85
- # Instantiates and executes the tool and returns the answer
86
- #
87
- # @param input [String] input to the tool
88
- # @return [String] answer
89
- #
90
- def self.execute(input:)
91
- new.execute(input: input)
92
- end
93
-
94
- #
95
- # Executes the tool and returns the answer
96
- #
97
- # @param input [String] input to the tool
98
- # @return [String] answer
99
- # @raise NotImplementedError when not implemented
100
- def execute(input:)
101
- raise NotImplementedError, "Your tool must implement the `#execute(input:)` method that returns a string"
102
- end
103
-
104
- #
105
- # Validates the list of tools or raises an error
106
- # @param tools [Array<Langchain::Tool>] list of tools to be used
107
- #
108
- # @raise [ArgumentError] If any of the tools are not supported
109
- #
110
- def self.validate_tools!(tools:)
111
- # Check if the tool count is equal to unique tool count
112
- if tools.count != tools.map(&:name).uniq.count
113
- raise ArgumentError, "Either tools are not unique or are conflicting with each other"
114
- end
74
+ # Return tool's method annotations as JSON
75
+ #
76
+ # @return [Hash] Tool's method annotations
77
+ def method_annotations
78
+ JSON.parse(
79
+ File.read(
80
+ self.class.const_get(:ANNOTATIONS_PATH)
81
+ )
82
+ )
115
83
  end
116
84
  end
117
85
  end
@@ -0,0 +1,19 @@
1
+ [
2
+ {
3
+ "type": "function",
4
+ "function": {
5
+ "name": "calculator-execute",
6
+ "description": "Evaluates a pure math expression or if equation contains non-math characters (e.g.: \"12F in Celsius\") then it uses the google search calculator to evaluate the expression",
7
+ "parameters": {
8
+ "type": "object",
9
+ "properties": {
10
+ "input": {
11
+ "type": "string",
12
+ "description": "math expression"
13
+ }
14
+ },
15
+ "required": ["input"]
16
+ }
17
+ }
18
+ }
19
+ ]
@@ -6,29 +6,21 @@ module Langchain::Tool
6
6
  # A calculator tool that falls back to the Google calculator widget
7
7
  #
8
8
  # Gem requirements:
9
- # gem "eqn", "~> 1.6.5"
10
- # gem "google_search_results", "~> 2.0.0"
9
+ # gem "eqn", "~> 1.6.5"
10
+ # gem "google_search_results", "~> 2.0.0"
11
+ #
12
+ # Usage:
13
+ # calculator = Langchain::Tool::Calculator.new
11
14
  #
12
-
13
15
  NAME = "calculator"
14
-
15
- description <<~DESC
16
- Useful for getting the result of a math expression.
17
-
18
- The input to this tool should be a valid mathematical expression that could be executed by a simple calculator.
19
- Usage:
20
- Action Input: 1 + 1
21
- Action Input: 3 * 2 / 4
22
- Action Input: 9 - 7
23
- Action Input: (4.1 + 2.3) / (2.0 - 5.6) * 3
24
- DESC
16
+ ANNOTATIONS_PATH = Langchain.root.join("./langchain/tool/#{NAME}/#{NAME}.json").to_path
25
17
 
26
18
  def initialize
27
19
  depends_on "eqn"
28
20
  end
29
21
 
30
- # Evaluates a pure math expression or if equation contains non-math characters (e.g.: "12F in Celsius") then
31
- # it uses the google search calculator to evaluate the expression
22
+ # Evaluates a pure math expression or if equation contains non-math characters (e.g.: "12F in Celsius") then it uses the google search calculator to evaluate the expression
23
+ #
32
24
  # @param input [String] math expression
33
25
  # @return [String] Answer
34
26
  def execute(input:)
@@ -0,0 +1,46 @@
1
+ [
2
+ {
3
+ "type": "function",
4
+ "function": {
5
+ "name": "database-describe_tables",
6
+ "description": "Database Tool: Returns the schema for a list of tables",
7
+ "parameters": {
8
+ "type": "object",
9
+ "properties": {
10
+ "tables": {
11
+ "type": "string",
12
+ "description": "The tables to describe."
13
+ }
14
+ },
15
+ "required": ["tables"]
16
+ }
17
+ }
18
+ }, {
19
+ "type": "function",
20
+ "function": {
21
+ "name": "database-list_tables",
22
+ "description": "Database Tool: Returns a list of tables in the database",
23
+ "parameters": {
24
+ "type": "object",
25
+ "properties": {},
26
+ "required": []
27
+ }
28
+ }
29
+ }, {
30
+ "type": "function",
31
+ "function": {
32
+ "name": "database-execute",
33
+ "description": "Database Tool: Executes a SQL query and returns the results",
34
+ "parameters": {
35
+ "type": "object",
36
+ "properties": {
37
+ "input": {
38
+ "type": "string",
39
+ "description": "SQL query to be executed"
40
+ }
41
+ },
42
+ "required": ["input"]
43
+ }
44
+ }
45
+ }
46
+ ]
@@ -0,0 +1,99 @@
1
+ module Langchain::Tool
2
+ class Database < Base
3
+ #
4
+ # Connects to a database, executes SQL queries, and outputs DB schema for Agents to use
5
+ #
6
+ # Gem requirements:
7
+ # gem "sequel", "~> 5.68.0"
8
+ #
9
+ # Usage:
10
+ # database = Langchain::Tool::Database.new(connection_string: "postgres://user:password@localhost:5432/db_name")
11
+ #
12
+ NAME = "database"
13
+ ANNOTATIONS_PATH = Langchain.root.join("./langchain/tool/#{NAME}/#{NAME}.json").to_path
14
+
15
+ attr_reader :db, :requested_tables, :excluded_tables
16
+
17
+ # Establish a database connection
18
+ #
19
+ # @param connection_string [String] Database connection info, e.g. 'postgres://user:password@localhost:5432/db_name'
20
+ # @param tables [Array<Symbol>] The tables to use. Will use all if empty.
21
+ # @param except_tables [Array<Symbol>] The tables to exclude. Will exclude none if empty.
22
+ # @return [Database] Database object
23
+ def initialize(connection_string:, tables: [], exclude_tables: [])
24
+ depends_on "sequel"
25
+
26
+ raise StandardError, "connection_string parameter cannot be blank" if connection_string.empty?
27
+
28
+ @db = Sequel.connect(connection_string)
29
+ @requested_tables = tables
30
+ @excluded_tables = exclude_tables
31
+ end
32
+
33
+ # Database Tool: Returns a list of tables in the database
34
+ def list_tables
35
+ db.tables
36
+ end
37
+
38
+ # Database Tool: Returns the schema for a list of tables
39
+ #
40
+ # @param tables [String] The tables to describe.
41
+ # @return [String] Database schema for the tables
42
+ def describe_tables(tables:)
43
+ schema = ""
44
+ tables.split(",").each do |table|
45
+ describe_table(table, schema)
46
+ end
47
+ schema
48
+ end
49
+
50
+ # Database Tool: Returns the database schema
51
+ #
52
+ # @return [String] Database schema
53
+ def dump_schema
54
+ Langchain.logger.info("Dumping schema tables and keys", for: self.class)
55
+ schema = ""
56
+ db.tables.each do |table|
57
+ describe_table(table, schema)
58
+ end
59
+ schema
60
+ end
61
+
62
+ def describe_table(table, schema)
63
+ primary_key_columns = []
64
+ primary_key_column_count = db.schema(table).count { |column| column[1][:primary_key] == true }
65
+
66
+ schema << "CREATE TABLE #{table}(\n"
67
+ db.schema(table).each do |column|
68
+ schema << "#{column[0]} #{column[1][:type]}"
69
+ if column[1][:primary_key] == true
70
+ schema << " PRIMARY KEY" if primary_key_column_count == 1
71
+ else
72
+ primary_key_columns << column[0]
73
+ end
74
+ schema << ",\n" unless column == db.schema(table).last && primary_key_column_count == 1
75
+ end
76
+ if primary_key_column_count > 1
77
+ schema << "PRIMARY KEY (#{primary_key_columns.join(",")})"
78
+ end
79
+ db.foreign_key_list(table).each do |fk|
80
+ schema << ",\n" if fk == db.foreign_key_list(table).first
81
+ schema << "FOREIGN KEY (#{fk[:columns][0]}) REFERENCES #{fk[:table]}(#{fk[:key][0]})"
82
+ schema << ",\n" unless fk == db.foreign_key_list(table).last
83
+ end
84
+ schema << ");\n"
85
+ end
86
+
87
+ # Database Tool: Executes a SQL query and returns the results
88
+ #
89
+ # @param input [String] SQL query to be executed
90
+ # @return [Array] Results from the SQL query
91
+ def execute(input:)
92
+ Langchain.logger.info("Executing \"#{input}\"", for: self.class)
93
+
94
+ db[input].to_a
95
+ rescue Sequel::DatabaseError => e
96
+ Langchain.logger.error(e.message, for: self.class)
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,57 @@
1
+ [
2
+ {
3
+ "type": "function",
4
+ "function": {
5
+ "name": "file_system-list_directory",
6
+ "description": "File System Tool: Lists out the content of a specified directory",
7
+ "parameters": {
8
+ "type": "object",
9
+ "properties": {
10
+ "directory_path": {
11
+ "type": "string",
12
+ "description": "Directory path to list"
13
+ }
14
+ },
15
+ "required": ["directory_path"]
16
+ }
17
+ }
18
+ },
19
+ {
20
+ "type": "function",
21
+ "function": {
22
+ "name": "file_system-read_file",
23
+ "description": "File System Tool: Reads the contents of a file",
24
+ "parameters": {
25
+ "type": "object",
26
+ "properties": {
27
+ "file_path": {
28
+ "type": "string",
29
+ "description": "Path to the file to read from"
30
+ }
31
+ },
32
+ "required": ["file_path"]
33
+ }
34
+ }
35
+ },
36
+ {
37
+ "type": "function",
38
+ "function": {
39
+ "name": "file_system-write_to_file",
40
+ "description": "File System Tool: Write content to a file",
41
+ "parameters": {
42
+ "type": "object",
43
+ "properties": {
44
+ "file_path": {
45
+ "type": "string",
46
+ "description": "Path to the file to write"
47
+ },
48
+ "content": {
49
+ "type": "string",
50
+ "description": "Content to write to the file"
51
+ }
52
+ },
53
+ "required": ["file_path", "content"]
54
+ }
55
+ }
56
+ }
57
+ ]