raif 1.0.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/MIT-LICENSE +20 -0
- data/README.md +678 -0
- data/Rakefile +20 -0
- data/app/assets/builds/raif.css +74 -0
- data/app/assets/builds/raif_admin.css +266 -0
- data/app/assets/config/raif_manifest.js +1 -0
- data/app/assets/javascript/raif/controllers/conversations_controller.js +11 -0
- data/app/assets/javascript/raif/stream_actions/raif_scroll_to_bottom.js +12 -0
- data/app/assets/javascript/raif.js +10 -0
- data/app/assets/stylesheets/raif/admin/conversation.scss +64 -0
- data/app/assets/stylesheets/raif/loader.scss +85 -0
- data/app/assets/stylesheets/raif.scss +1 -0
- data/app/assets/stylesheets/raif_admin.scss +299 -0
- data/app/controllers/raif/admin/agents_controller.rb +17 -0
- data/app/controllers/raif/admin/application_controller.rb +20 -0
- data/app/controllers/raif/admin/conversations_controller.rb +17 -0
- data/app/controllers/raif/admin/model_completions_controller.rb +17 -0
- data/app/controllers/raif/admin/model_tool_invocations_controller.rb +17 -0
- data/app/controllers/raif/admin/tasks_controller.rb +23 -0
- data/app/controllers/raif/application_controller.rb +20 -0
- data/app/controllers/raif/conversation_entries_controller.rb +60 -0
- data/app/controllers/raif/conversations_controller.rb +58 -0
- data/app/helpers/raif/application_helper.rb +7 -0
- data/app/helpers/raif/shared/conversations_helper.rb +13 -0
- data/app/jobs/raif/application_job.rb +8 -0
- data/app/jobs/raif/conversation_entry_job.rb +30 -0
- data/app/models/raif/agent.rb +133 -0
- data/app/models/raif/agents/native_tool_calling_agent.rb +127 -0
- data/app/models/raif/agents/re_act_agent.rb +121 -0
- data/app/models/raif/agents/re_act_step.rb +33 -0
- data/app/models/raif/application_record.rb +14 -0
- data/app/models/raif/concerns/boolean_timestamp.rb +69 -0
- data/app/models/raif/concerns/has_available_model_tools.rb +13 -0
- data/app/models/raif/concerns/has_llm.rb +19 -0
- data/app/models/raif/concerns/has_requested_language.rb +20 -0
- data/app/models/raif/concerns/invokes_model_tools.rb +13 -0
- data/app/models/raif/concerns/llm_response_parsing.rb +44 -0
- data/app/models/raif/conversation.rb +67 -0
- data/app/models/raif/conversation_entry.rb +85 -0
- data/app/models/raif/llm.rb +88 -0
- data/app/models/raif/llms/anthropic.rb +120 -0
- data/app/models/raif/llms/bedrock_claude.rb +134 -0
- data/app/models/raif/llms/open_ai.rb +259 -0
- data/app/models/raif/model_completion.rb +28 -0
- data/app/models/raif/model_tool.rb +69 -0
- data/app/models/raif/model_tool_invocation.rb +43 -0
- data/app/models/raif/model_tools/agent_final_answer.rb +46 -0
- data/app/models/raif/model_tools/fetch_url.rb +57 -0
- data/app/models/raif/model_tools/wikipedia_search.rb +78 -0
- data/app/models/raif/task.rb +137 -0
- data/app/models/raif/user_tool_invocation.rb +29 -0
- data/app/views/layouts/raif/admin.html.erb +98 -0
- data/app/views/raif/admin/agents/_agent.html.erb +18 -0
- data/app/views/raif/admin/agents/_conversation_message.html.erb +15 -0
- data/app/views/raif/admin/agents/index.html.erb +33 -0
- data/app/views/raif/admin/agents/show.html.erb +131 -0
- data/app/views/raif/admin/conversations/_conversation.html.erb +7 -0
- data/app/views/raif/admin/conversations/_conversation_entry.html.erb +34 -0
- data/app/views/raif/admin/conversations/index.html.erb +32 -0
- data/app/views/raif/admin/conversations/show.html.erb +56 -0
- data/app/views/raif/admin/model_completions/_model_completion.html.erb +9 -0
- data/app/views/raif/admin/model_completions/index.html.erb +34 -0
- data/app/views/raif/admin/model_completions/show.html.erb +117 -0
- data/app/views/raif/admin/model_tool_invocations/_model_tool_invocation.html.erb +16 -0
- data/app/views/raif/admin/model_tool_invocations/index.html.erb +33 -0
- data/app/views/raif/admin/model_tool_invocations/show.html.erb +66 -0
- data/app/views/raif/admin/tasks/_task.html.erb +19 -0
- data/app/views/raif/admin/tasks/index.html.erb +49 -0
- data/app/views/raif/admin/tasks/show.html.erb +176 -0
- data/app/views/raif/conversation_entries/_conversation_entry.html.erb +26 -0
- data/app/views/raif/conversation_entries/_form.html.erb +25 -0
- data/app/views/raif/conversation_entries/_form_with_available_tools.html.erb +4 -0
- data/app/views/raif/conversation_entries/_form_with_user_tool_invocation.html.erb +18 -0
- data/app/views/raif/conversation_entries/_message.html.erb +17 -0
- data/app/views/raif/conversation_entries/_model_response_avatar.html.erb +1 -0
- data/app/views/raif/conversation_entries/_user_avatar.html.erb +1 -0
- data/app/views/raif/conversation_entries/create.turbo_stream.erb +11 -0
- data/app/views/raif/conversation_entries/new.turbo_stream.erb +6 -0
- data/app/views/raif/conversations/_available_user_tools.html.erb +11 -0
- data/app/views/raif/conversations/_full_conversation.html.erb +15 -0
- data/app/views/raif/conversations/show.html.erb +1 -0
- data/config/i18n-tasks.yml +181 -0
- data/config/importmap.rb +6 -0
- data/config/initializers/pagy.rb +14 -0
- data/config/locales/admin.en.yml +91 -0
- data/config/locales/en.yml +50 -0
- data/config/routes.rb +22 -0
- data/db/migrate/20250224234252_create_raif_tables.rb +114 -0
- data/lib/generators/raif/agent/agent_generator.rb +22 -0
- data/lib/generators/raif/agent/templates/agent.rb.tt +28 -0
- data/lib/generators/raif/conversation/conversation_generator.rb +27 -0
- data/lib/generators/raif/conversation/templates/conversation.rb.tt +37 -0
- data/lib/generators/raif/install/install_generator.rb +31 -0
- data/lib/generators/raif/install/templates/initializer.rb +81 -0
- data/lib/generators/raif/model_tool/model_tool_generator.rb +27 -0
- data/lib/generators/raif/model_tool/templates/model_tool.rb.tt +74 -0
- data/lib/generators/raif/task/task_generator.rb +28 -0
- data/lib/generators/raif/task/templates/application_task.rb.tt +7 -0
- data/lib/generators/raif/task/templates/task.rb.tt +52 -0
- data/lib/generators/raif/views_generator.rb +22 -0
- data/lib/raif/configuration.rb +82 -0
- data/lib/raif/default_llms.rb +37 -0
- data/lib/raif/engine.rb +86 -0
- data/lib/raif/errors/action_not_authorized_error.rb +8 -0
- data/lib/raif/errors/anthropic/api_error.rb +10 -0
- data/lib/raif/errors/invalid_config_error.rb +8 -0
- data/lib/raif/errors/invalid_conversation_type_error.rb +8 -0
- data/lib/raif/errors/invalid_user_tool_type_error.rb +8 -0
- data/lib/raif/errors/open_ai/api_error.rb +10 -0
- data/lib/raif/errors/open_ai/json_schema_error.rb +10 -0
- data/lib/raif/errors.rb +9 -0
- data/lib/raif/languages.rb +33 -0
- data/lib/raif/rspec.rb +7 -0
- data/lib/raif/utils/html_to_markdown_converter.rb +7 -0
- data/lib/raif/utils/readable_content_extractor.rb +61 -0
- data/lib/raif/utils.rb +6 -0
- data/lib/raif/version.rb +5 -0
- data/lib/raif.rb +65 -0
- data/lib/tasks/raif_tasks.rake +6 -0
- metadata +294 -0
@@ -0,0 +1,133 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Raif
|
4
|
+
class Agent < ApplicationRecord
|
5
|
+
include Raif::Concerns::HasLlm
|
6
|
+
include Raif::Concerns::HasRequestedLanguage
|
7
|
+
include Raif::Concerns::HasAvailableModelTools
|
8
|
+
include Raif::Concerns::InvokesModelTools
|
9
|
+
|
10
|
+
belongs_to :creator, polymorphic: true
|
11
|
+
|
12
|
+
has_many :raif_model_completions, as: :source, dependent: :destroy, class_name: "Raif::ModelCompletion"
|
13
|
+
|
14
|
+
after_initialize -> { self.available_model_tools ||= [] }
|
15
|
+
after_initialize -> { self.conversation_history ||= [] }
|
16
|
+
|
17
|
+
boolean_timestamp :started_at
|
18
|
+
boolean_timestamp :completed_at
|
19
|
+
boolean_timestamp :failed_at
|
20
|
+
|
21
|
+
validates :type, inclusion: { in: ->{ Raif.config.agent_types } }
|
22
|
+
validates :task, presence: true
|
23
|
+
validates :system_prompt, presence: true
|
24
|
+
validates :max_iterations, presence: true, numericality: { greater_than: 0 }
|
25
|
+
validates :available_model_tools, length: {
|
26
|
+
minimum: 1,
|
27
|
+
message: ->(_object, _data) {
|
28
|
+
I18n.t("raif.agents.errors.available_model_tools.too_short")
|
29
|
+
}
|
30
|
+
}
|
31
|
+
|
32
|
+
before_validation -> { self.system_prompt ||= build_system_prompt }, on: :create
|
33
|
+
|
34
|
+
attr_accessor :on_conversation_history_entry
|
35
|
+
|
36
|
+
# Runs the agent and returns a Raif::Agent.
|
37
|
+
# If a block is given, it will be called each time a new entry is added to the agent's conversation history.
|
38
|
+
# The block will receive the Raif::Agent and the new entry as arguments:
|
39
|
+
# agent = Raif::Agent.new(
|
40
|
+
# task: task,
|
41
|
+
# tools: [Raif::ModelTools::WikipediaSearch, Raif::ModelTools::FetchUrl],
|
42
|
+
# creator: creator
|
43
|
+
# )
|
44
|
+
#
|
45
|
+
# agent.run! do |conversation_history_entry|
|
46
|
+
# Turbo::StreamsChannel.broadcast_append_to(
|
47
|
+
# :my_agent_channel,
|
48
|
+
# target: "agent-progress",
|
49
|
+
# partial: "my_partial_displaying_agent_progress",
|
50
|
+
# locals: { agent: agent, conversation_history_entry: conversation_history_entry }
|
51
|
+
# )
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# The conversation_history_entry will be a hash with "role" and "content" keys:
|
55
|
+
# { "role" => "assistant", "content" => "a message here" }
|
56
|
+
#
|
57
|
+
# @param block [Proc] Optional block to be called each time a new entry to the agent's conversation history is generated
|
58
|
+
# @return [Raif::Agent] The agent that was created and run
|
59
|
+
def run!(&block)
|
60
|
+
self.on_conversation_history_entry = block_given? ? block : nil
|
61
|
+
self.started_at = Time.current
|
62
|
+
self.available_model_tools += ["Raif::ModelTools::AgentFinalAnswer"] unless available_model_tools.include?("Raif::ModelTools::AgentFinalAnswer")
|
63
|
+
save!
|
64
|
+
|
65
|
+
logger.debug <<~DEBUG
|
66
|
+
--------------------------------
|
67
|
+
Starting Agent Run
|
68
|
+
--------------------------------
|
69
|
+
System Prompt:
|
70
|
+
#{system_prompt}
|
71
|
+
|
72
|
+
Task: #{task}
|
73
|
+
DEBUG
|
74
|
+
|
75
|
+
add_conversation_history_entry({ role: "user", content: task })
|
76
|
+
|
77
|
+
while iteration_count < max_iterations
|
78
|
+
update_columns(iteration_count: iteration_count + 1)
|
79
|
+
|
80
|
+
model_completion = llm.chat(
|
81
|
+
messages: conversation_history,
|
82
|
+
source: self,
|
83
|
+
system_prompt: system_prompt,
|
84
|
+
available_model_tools: native_model_tools
|
85
|
+
)
|
86
|
+
|
87
|
+
logger.debug <<~DEBUG
|
88
|
+
--------------------------------
|
89
|
+
Agent iteration #{iteration_count}
|
90
|
+
Messages:
|
91
|
+
#{JSON.pretty_generate(conversation_history)}
|
92
|
+
|
93
|
+
Response:
|
94
|
+
#{model_completion.raw_response}
|
95
|
+
--------------------------------
|
96
|
+
DEBUG
|
97
|
+
|
98
|
+
process_iteration_model_completion(model_completion)
|
99
|
+
break if final_answer.present?
|
100
|
+
end
|
101
|
+
|
102
|
+
completed!
|
103
|
+
final_answer
|
104
|
+
rescue StandardError => e
|
105
|
+
self.failed_at = Time.current
|
106
|
+
self.failure_reason = e.message
|
107
|
+
save!
|
108
|
+
|
109
|
+
raise
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
def process_iteration_model_completion(model_completion)
|
115
|
+
raise NotImplementedError, "#{self.class.name} must implement execute_agent_iteration"
|
116
|
+
end
|
117
|
+
|
118
|
+
def native_model_tools
|
119
|
+
# no-op by default
|
120
|
+
end
|
121
|
+
|
122
|
+
def add_conversation_history_entry(entry)
|
123
|
+
entry_stringified = entry.stringify_keys
|
124
|
+
conversation_history << entry_stringified
|
125
|
+
on_conversation_history_entry.call(entry_stringified) if on_conversation_history_entry.present?
|
126
|
+
end
|
127
|
+
|
128
|
+
def build_system_prompt
|
129
|
+
raise NotImplementedError, "Subclasses of Raif::Agent must implement build_system_prompt"
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Raif
|
4
|
+
module Agents
|
5
|
+
class NativeToolCallingAgent < Raif::Agent
|
6
|
+
validate :ensure_llm_supports_native_tool_use
|
7
|
+
|
8
|
+
def build_system_prompt
|
9
|
+
<<~PROMPT.strip
|
10
|
+
You are an AI agent that follows the ReAct (Reasoning + Acting) framework to complete tasks step by step using tool/function calls.
|
11
|
+
|
12
|
+
At each step, you must:
|
13
|
+
1. Think about what to do next.
|
14
|
+
2. Choose and invoke exactly one tool/function call based on that thought.
|
15
|
+
3. Observe the results of the tool/function call.
|
16
|
+
4. Use the results to update your thought process.
|
17
|
+
5. Repeat steps 1-4 until the task is complete.
|
18
|
+
6. Provide a final answer to the user's request.
|
19
|
+
|
20
|
+
For your final answer:
|
21
|
+
- Use the agent_final_answer tool/function with your complete answer as the "final_answer" parameter.
|
22
|
+
- Your answer should be comprehensive and directly address the user's request.
|
23
|
+
|
24
|
+
Guidelines
|
25
|
+
- Always think step by step
|
26
|
+
- Be concise in your reasoning but thorough in your analysis
|
27
|
+
- If a tool returns an error, try to understand why and adjust your approach
|
28
|
+
- If you're unsure about something, explain your uncertainty, but do not make things up
|
29
|
+
- Always provide a final answer that directly addresses the user's request
|
30
|
+
|
31
|
+
Remember: Your goal is to be helpful, accurate, and efficient in solving the user's request.#{system_prompt_language_preference}
|
32
|
+
PROMPT
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def native_model_tools
|
38
|
+
available_model_tools
|
39
|
+
end
|
40
|
+
|
41
|
+
def process_iteration_model_completion(model_completion)
|
42
|
+
if model_completion.parsed_response.present?
|
43
|
+
add_conversation_history_entry({
|
44
|
+
role: "assistant",
|
45
|
+
content: model_completion.parsed_response
|
46
|
+
})
|
47
|
+
end
|
48
|
+
|
49
|
+
if model_completion.response_tool_calls.blank?
|
50
|
+
add_conversation_history_entry({
|
51
|
+
role: "assistant",
|
52
|
+
content: "<observation>Error: No tool call found. I need make a tool call at each step. Available tools: #{available_model_tools_map.keys.join(", ")}</observation>" # rubocop:disable Layout/LineLength
|
53
|
+
})
|
54
|
+
return
|
55
|
+
end
|
56
|
+
|
57
|
+
tool_call = model_completion.response_tool_calls.first
|
58
|
+
|
59
|
+
unless tool_call["name"] && tool_call["arguments"]
|
60
|
+
add_conversation_history_entry({
|
61
|
+
role: "assistant",
|
62
|
+
content: "<observation>Error: Invalid action specified. Please provide a valid action, formatted as a JSON object with 'tool' and 'arguments' keys.</observation>" # rubocop:disable Layout/LineLength
|
63
|
+
})
|
64
|
+
return
|
65
|
+
end
|
66
|
+
|
67
|
+
tool_name = tool_call["name"]
|
68
|
+
tool_arguments = tool_call["arguments"]
|
69
|
+
|
70
|
+
# Add assistant's response to conversation history (without the actual tool calls)
|
71
|
+
# add_conversation_history_entry({
|
72
|
+
# role: "assistant",
|
73
|
+
# content: "<thought>I need to use the #{tool_name} tool to help with this task.</thought>"
|
74
|
+
# })
|
75
|
+
|
76
|
+
# Check if we have a final answer. If yes, we're done.
|
77
|
+
if tool_name == "agent_final_answer"
|
78
|
+
self.final_answer = tool_arguments["final_answer"]
|
79
|
+
add_conversation_history_entry({ role: "assistant", content: "<answer>#{final_answer}</answer>" })
|
80
|
+
return
|
81
|
+
end
|
82
|
+
|
83
|
+
# Add the tool call to conversation history
|
84
|
+
add_conversation_history_entry({
|
85
|
+
role: "assistant",
|
86
|
+
content: "<action>#{JSON.pretty_generate(tool_call)}</action>"
|
87
|
+
})
|
88
|
+
|
89
|
+
# Find the tool class and process it
|
90
|
+
tool_klass = available_model_tools_map[tool_name]
|
91
|
+
|
92
|
+
# The model tried to use a tool that doesn't exist
|
93
|
+
unless tool_klass
|
94
|
+
add_conversation_history_entry({
|
95
|
+
role: "assistant",
|
96
|
+
content: "<observation>Error: Tool '#{tool_name}' not found. Available tools: #{available_model_tools_map.keys.join(", ")}</observation>"
|
97
|
+
})
|
98
|
+
return
|
99
|
+
end
|
100
|
+
|
101
|
+
unless JSON::Validator.validate(tool_klass.tool_arguments_schema, tool_arguments)
|
102
|
+
add_conversation_history_entry({
|
103
|
+
role: "assistant",
|
104
|
+
content: "<observation>Error: Invalid tool arguments. Please provide valid arguments for the tool '#{tool_name}'. Tool arguments schema: #{tool_klass.tool_arguments_schema.to_json}</observation>" # rubocop:disable Layout/LineLength
|
105
|
+
})
|
106
|
+
return
|
107
|
+
end
|
108
|
+
|
109
|
+
# Process the tool and add observation to history
|
110
|
+
tool_invocation = tool_klass.invoke_tool(tool_arguments: tool_arguments, source: self)
|
111
|
+
observation = tool_klass.observation_for_invocation(tool_invocation)
|
112
|
+
|
113
|
+
add_conversation_history_entry({
|
114
|
+
role: "assistant",
|
115
|
+
content: "<observation>#{observation}</observation>"
|
116
|
+
})
|
117
|
+
end
|
118
|
+
|
119
|
+
def ensure_llm_supports_native_tool_use
|
120
|
+
unless llm.supports_native_tool_use?
|
121
|
+
errors.add(:base, "Raif::Agent#llm_model_key must use an LLM that supports native tool use")
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Raif
|
4
|
+
module Agents
|
5
|
+
class ReActAgent < Raif::Agent
|
6
|
+
|
7
|
+
def build_system_prompt
|
8
|
+
<<~PROMPT.strip
|
9
|
+
You are an intelligent assistant that follows the ReAct (Reasoning + Acting) framework to complete tasks step by step using tool calls.
|
10
|
+
|
11
|
+
# Available Tools
|
12
|
+
You have access to the following tools:
|
13
|
+
#{available_model_tools_map.values.map(&:description_for_llm).join("\n---\n")}
|
14
|
+
# Your Responses
|
15
|
+
Your responses should follow this structure & format:
|
16
|
+
<thought>Your step-by-step reasoning about what to do</thought>
|
17
|
+
<action>JSON object with "tool" and "arguments" keys</action>
|
18
|
+
<observation>Results from the tool, which will be provided to you</observation>
|
19
|
+
... (repeat Thought/Action/Observation as needed until the task is complete)
|
20
|
+
<thought>Final reasoning based on all observations</thought>
|
21
|
+
<answer>Your final response to the user</answer>
|
22
|
+
|
23
|
+
# How to Use Tools
|
24
|
+
When you need to use a tool:
|
25
|
+
1. Identify which tool is appropriate for the task
|
26
|
+
2. Format your tool call using JSON with the required arguments and place it in the <action> tag
|
27
|
+
3. Here is an example: <action>{"tool": "tool_name", "arguments": {...}}</action>
|
28
|
+
|
29
|
+
# Guidelines
|
30
|
+
- Always think step by step
|
31
|
+
- Use tools when appropriate, but don't use tools for tasks you can handle directly
|
32
|
+
- Be concise in your reasoning but thorough in your analysis
|
33
|
+
- If a tool returns an error, try to understand why and adjust your approach
|
34
|
+
- If you're unsure about something, explain your uncertainty, but do not make things up
|
35
|
+
- After each thought, make sure to also include an <action> or <answer>
|
36
|
+
- Always provide a final answer that directly addresses the user's request
|
37
|
+
|
38
|
+
Remember: Your goal is to be helpful, accurate, and efficient in solving the user's request.#{system_prompt_language_preference}
|
39
|
+
PROMPT
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def process_iteration_model_completion(model_completion)
|
45
|
+
agent_step = Raif::Agents::ReActStep.new(model_response_text: model_completion.raw_response)
|
46
|
+
|
47
|
+
# Add the thought to conversation history
|
48
|
+
if agent_step.thought
|
49
|
+
add_conversation_history_entry({ role: "assistant", content: "<thought>#{agent_step.thought}</thought>" })
|
50
|
+
end
|
51
|
+
|
52
|
+
# If there's an answer, we're done
|
53
|
+
if agent_step.answer
|
54
|
+
self.final_answer = agent_step.answer
|
55
|
+
add_conversation_history_entry({ role: "assistant", content: "<answer>#{agent_step.answer}</answer>" })
|
56
|
+
return
|
57
|
+
end
|
58
|
+
|
59
|
+
# If there's an action, execute it
|
60
|
+
process_action(agent_step.action) if agent_step.action
|
61
|
+
end
|
62
|
+
|
63
|
+
def process_action(action)
|
64
|
+
add_conversation_history_entry({ role: "assistant", content: "<action>#{action}</action>" })
|
65
|
+
|
66
|
+
# The action should always be a JSON object with "tool" and "arguments" keys
|
67
|
+
parsed_action = begin
|
68
|
+
JSON.parse(action)
|
69
|
+
rescue JSON::ParserError => e
|
70
|
+
add_conversation_history_entry({
|
71
|
+
role: "assistant",
|
72
|
+
content: "<observation>Error parsing action JSON: #{e.message}</observation>"
|
73
|
+
})
|
74
|
+
|
75
|
+
nil
|
76
|
+
end
|
77
|
+
|
78
|
+
return if parsed_action.blank?
|
79
|
+
|
80
|
+
unless parsed_action["tool"] && parsed_action["arguments"]
|
81
|
+
add_conversation_history_entry({
|
82
|
+
role: "assistant",
|
83
|
+
content: "<observation>Error: Invalid action specified. Please provide a valid action, formatted as a JSON object with 'tool' and 'arguments' keys.</observation>" # rubocop:disable Layout/LineLength
|
84
|
+
})
|
85
|
+
return
|
86
|
+
end
|
87
|
+
|
88
|
+
tool_name = parsed_action["tool"]
|
89
|
+
tool_arguments = parsed_action["arguments"]
|
90
|
+
tool_klass = available_model_tools_map[tool_name]
|
91
|
+
|
92
|
+
# The model tried to use a tool that doesn't exist
|
93
|
+
unless tool_klass
|
94
|
+
add_conversation_history_entry({
|
95
|
+
role: "assistant",
|
96
|
+
content: "<observation>Error: Tool '#{tool_name}' not found. Available tools: #{available_model_tools_map.keys.join(", ")}</observation>"
|
97
|
+
})
|
98
|
+
return
|
99
|
+
end
|
100
|
+
|
101
|
+
unless JSON::Validator.validate(tool_klass.tool_arguments_schema, tool_arguments)
|
102
|
+
add_conversation_history_entry({
|
103
|
+
role: "assistant",
|
104
|
+
content: "<observation>Error: Invalid tool arguments. Please provide valid arguments for the tool '#{tool_name}'. Tool arguments schema: #{tool_klass.tool_arguments_schema.to_json}</observation>" # rubocop:disable Layout/LineLength
|
105
|
+
})
|
106
|
+
return
|
107
|
+
end
|
108
|
+
|
109
|
+
tool_invocation = tool_klass.invoke_tool(tool_arguments: tool_arguments, source: self)
|
110
|
+
observation = tool_klass.observation_for_invocation(tool_invocation)
|
111
|
+
|
112
|
+
# Add the tool invocation to conversation history
|
113
|
+
add_conversation_history_entry({
|
114
|
+
role: "assistant",
|
115
|
+
content: "<observation>#{observation}</observation>"
|
116
|
+
})
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Raif
|
4
|
+
module Agents
|
5
|
+
class ReActStep
|
6
|
+
attr_reader :model_response_text
|
7
|
+
|
8
|
+
def initialize(model_response_text:)
|
9
|
+
@model_response_text = model_response_text
|
10
|
+
end
|
11
|
+
|
12
|
+
def thought
|
13
|
+
@thought ||= extract_tag_content("thought")
|
14
|
+
end
|
15
|
+
|
16
|
+
def answer
|
17
|
+
@answer ||= extract_tag_content("answer")
|
18
|
+
end
|
19
|
+
|
20
|
+
def action
|
21
|
+
@action ||= extract_tag_content("action")
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def extract_tag_content(tag_name)
|
27
|
+
match = model_response_text.match(%r{<#{tag_name}>(.*?)</#{tag_name}>}m)
|
28
|
+
match ? match[1].strip : nil
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Raif::ApplicationRecord < Raif.config.model_superclass.constantize
|
4
|
+
include Raif::Concerns::BooleanTimestamp
|
5
|
+
|
6
|
+
self.abstract_class = true
|
7
|
+
|
8
|
+
scope :newest_first, -> { order(created_at: :desc) }
|
9
|
+
scope :oldest_first, -> { order(created_at: :asc) }
|
10
|
+
|
11
|
+
def self.table_name_prefix
|
12
|
+
"raif_"
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Raif::Concerns::BooleanTimestamp
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
class_methods do
|
7
|
+
# Column name should be something like email_abc_disabled_at, or its inverse should be specified
|
8
|
+
def boolean_timestamp(column_name, define_inverse_accessors: false)
|
9
|
+
if define_inverse_accessors.present?
|
10
|
+
unless define_inverse_accessors.is_a?(Symbol) || column_name.end_with?("_disabled_at") || column_name.end_with?("_enabled_at")
|
11
|
+
raise ArgumentError, "boolean_timestamp column name (#{column_name}) must end with '_disabled_at' or '_enabled_at'"
|
12
|
+
end
|
13
|
+
|
14
|
+
inverse_boolean_column_name = define_inverse_accessors if define_inverse_accessors.is_a?(Symbol)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Get the boolean version of the column name (e.g. email_abc_disabled)
|
18
|
+
boolean_column_name = column_name.to_s.gsub(/_at$/, "")
|
19
|
+
|
20
|
+
# Define boolean getter (e.g. email_abc_disabled?)
|
21
|
+
define_method("#{boolean_column_name}?") do
|
22
|
+
send(column_name).present?
|
23
|
+
end
|
24
|
+
alias_method boolean_column_name, "#{boolean_column_name}?"
|
25
|
+
|
26
|
+
# Define boolean setter (e.g. email_abc_disabled = true)
|
27
|
+
define_method("#{boolean_column_name}=") do |val|
|
28
|
+
if val == "1" || val == 1 || val == true || val == "true"
|
29
|
+
send("#{column_name}=", Time.current) if send(column_name).nil?
|
30
|
+
else
|
31
|
+
send("#{column_name}=", nil)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Define bang method to set the value (e.g. email_abc_disabled!)
|
36
|
+
define_method("#{boolean_column_name}!") do
|
37
|
+
update(column_name => Time.current) if send(column_name).nil?
|
38
|
+
end
|
39
|
+
|
40
|
+
scope boolean_column_name, -> { where.not(table_name => { column_name => nil }) }
|
41
|
+
|
42
|
+
# Define the inverse getter/setter (e.g. email_abc_enabled?)
|
43
|
+
if define_inverse_accessors
|
44
|
+
inverse_boolean_column_name ||= if boolean_column_name.end_with?("_disabled")
|
45
|
+
boolean_column_name.gsub(/_disabled$/, "_enabled")
|
46
|
+
else
|
47
|
+
boolean_column_name.gsub(/_enabled$/, "_disabled")
|
48
|
+
end
|
49
|
+
|
50
|
+
# Define boolean getter (e.g. email_abc_enabled)
|
51
|
+
define_method("#{inverse_boolean_column_name}?") do
|
52
|
+
!send(boolean_column_name)
|
53
|
+
end
|
54
|
+
alias_method inverse_boolean_column_name, "#{inverse_boolean_column_name}?"
|
55
|
+
|
56
|
+
# Define boolean setter (e.g. email_abc_enabled = true)
|
57
|
+
define_method("#{inverse_boolean_column_name}=") do |val|
|
58
|
+
if val == "1" || val == 1 || val == true || val == "true"
|
59
|
+
send("#{column_name}=", nil)
|
60
|
+
elsif send(column_name).nil?
|
61
|
+
send("#{column_name}=", Time.current)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
scope inverse_boolean_column_name, -> { where(table_name => { column_name => nil }) }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Raif::Concerns::HasAvailableModelTools
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
def available_model_tools_map
|
7
|
+
@available_model_tools_map ||= available_model_tools&.map do |tool_name|
|
8
|
+
tool_klass = tool_name.is_a?(String) ? tool_name.constantize : tool_name
|
9
|
+
[tool_klass.tool_name, tool_klass]
|
10
|
+
end.to_h
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Raif::Concerns::HasLlm
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
validates :llm_model_key, presence: true, inclusion: { in: ->{ Raif.available_llm_keys.map(&:to_s) } }
|
8
|
+
|
9
|
+
before_validation ->{ self.llm_model_key ||= default_llm_model_key }
|
10
|
+
end
|
11
|
+
|
12
|
+
def default_llm_model_key
|
13
|
+
Rails.env.test? ? :raif_test_llm : Raif.config.default_llm_model_key
|
14
|
+
end
|
15
|
+
|
16
|
+
def llm
|
17
|
+
@llm ||= Raif.llm(llm_model_key.to_sym)
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Raif::Concerns::HasRequestedLanguage
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
validates :requested_language_key, inclusion: { in: Raif.supported_languages, allow_blank: true }
|
8
|
+
end
|
9
|
+
|
10
|
+
def requested_language_name
|
11
|
+
@requested_language_name ||= I18n.t("raif.languages.#{requested_language_key}", locale: "en")
|
12
|
+
end
|
13
|
+
|
14
|
+
def system_prompt_language_preference
|
15
|
+
return if requested_language_key.blank?
|
16
|
+
|
17
|
+
"\nYou're collaborating with teammate who speaks #{requested_language_name}. Please respond in #{requested_language_name}."
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Raif::Concerns::InvokesModelTools
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
has_many :raif_model_tool_invocations,
|
8
|
+
class_name: "Raif::ModelToolInvocation",
|
9
|
+
as: :source,
|
10
|
+
dependent: :destroy
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Raif::Concerns::LlmResponseParsing
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
normalizes :raw_response, with: ->(text){ text&.strip }
|
8
|
+
|
9
|
+
enum :response_format, Raif::Llm.valid_response_formats, prefix: true
|
10
|
+
|
11
|
+
validates :response_format, presence: true, inclusion: { in: response_formats.keys }
|
12
|
+
end
|
13
|
+
|
14
|
+
# Parses the response from the LLM into a structured format, based on the response_format.
|
15
|
+
# If the response format is JSON, it will be parsed using JSON.parse.
|
16
|
+
# If the response format is HTML, it will be sanitized via ActionController::Base.helpers.sanitize.
|
17
|
+
#
|
18
|
+
# @return [Object] The parsed response.
|
19
|
+
def parsed_response
|
20
|
+
return if raw_response.blank?
|
21
|
+
|
22
|
+
@parsed_response ||= if response_format_json?
|
23
|
+
json = raw_response.gsub("```json", "").gsub("```", "")
|
24
|
+
JSON.parse(json)
|
25
|
+
elsif response_format_html?
|
26
|
+
html = raw_response.strip.gsub("```html", "").chomp("```")
|
27
|
+
clean_html_fragment(html)
|
28
|
+
else
|
29
|
+
raw_response.strip
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def clean_html_fragment(html)
|
34
|
+
fragment = Nokogiri::HTML.fragment(html)
|
35
|
+
|
36
|
+
fragment.traverse do |node|
|
37
|
+
if node.text? && node.text.strip.empty?
|
38
|
+
node.remove
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
ActionController::Base.helpers.sanitize(fragment.to_html).strip
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Raif::Conversation < Raif::ApplicationRecord
|
4
|
+
include Raif::Concerns::HasLlm
|
5
|
+
include Raif::Concerns::HasRequestedLanguage
|
6
|
+
include Raif::Concerns::HasAvailableModelTools
|
7
|
+
|
8
|
+
belongs_to :creator, polymorphic: true
|
9
|
+
|
10
|
+
has_many :entries, class_name: "Raif::ConversationEntry", dependent: :destroy, foreign_key: :raif_conversation_id, inverse_of: :raif_conversation
|
11
|
+
|
12
|
+
validates :type, inclusion: { in: ->{ Raif.config.conversation_types } }
|
13
|
+
|
14
|
+
after_initialize -> { self.available_model_tools ||= [] }
|
15
|
+
after_initialize -> { self.available_user_tools ||= [] }
|
16
|
+
|
17
|
+
before_validation ->{ self.type ||= "Raif::Conversation" }, on: :create
|
18
|
+
before_validation -> { self.system_prompt ||= build_system_prompt }, on: :create
|
19
|
+
|
20
|
+
def build_system_prompt
|
21
|
+
<<~PROMPT
|
22
|
+
#{system_prompt_intro}
|
23
|
+
#{system_prompt_language_preference}
|
24
|
+
PROMPT
|
25
|
+
end
|
26
|
+
|
27
|
+
def system_prompt_intro
|
28
|
+
Raif.config.conversation_system_prompt_intro
|
29
|
+
end
|
30
|
+
|
31
|
+
# i18n-tasks-use t('raif.conversation.initial_chat_message')
|
32
|
+
def initial_chat_message
|
33
|
+
I18n.t("#{self.class.name.underscore.gsub("/", ".")}.initial_chat_message")
|
34
|
+
end
|
35
|
+
|
36
|
+
def prompt_model_for_entry_response(entry:)
|
37
|
+
llm.chat(
|
38
|
+
messages: llm_messages,
|
39
|
+
source: entry,
|
40
|
+
response_format: :text,
|
41
|
+
system_prompt: system_prompt,
|
42
|
+
available_model_tools: available_model_tools
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
def llm_messages
|
47
|
+
messages = []
|
48
|
+
|
49
|
+
entries.oldest_first.includes(:raif_model_tool_invocations).each do |entry|
|
50
|
+
messages << { "role" => "user", "content" => entry.user_message }
|
51
|
+
next unless entry.completed?
|
52
|
+
|
53
|
+
messages << { "role" => "assistant", "content" => entry.model_response_message } unless entry.model_response_message.blank?
|
54
|
+
entry.raif_model_tool_invocations.each do |tool_invocation|
|
55
|
+
messages << { "role" => "assistant", "content" => tool_invocation.as_llm_message }
|
56
|
+
messages << { "role" => "assistant", "content" => tool_invocation.result_llm_message } if tool_invocation.result_llm_message.present?
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
messages
|
61
|
+
end
|
62
|
+
|
63
|
+
def available_user_tool_classes
|
64
|
+
available_user_tools.map(&:constantize)
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|