feat: Implementa e aprimora funcionalidades relacionadas a Captain e Jasmine, incluindo ferramentas, serviços LLM, integrações WhatsApp e ajustes de configuração.
This commit is contained in:
parent
a392d81f06
commit
2672d21136
@ -1,3 +1,9 @@
|
|||||||
|
inherit_from: .rubocop_todo.yml
|
||||||
|
|
||||||
|
inherit_mode:
|
||||||
|
merge:
|
||||||
|
- Exclude
|
||||||
|
|
||||||
plugins:
|
plugins:
|
||||||
- rubocop-performance
|
- rubocop-performance
|
||||||
- rubocop-rails
|
- rubocop-rails
|
||||||
@ -23,6 +29,7 @@ Metrics/MethodLength:
|
|||||||
Max: 19
|
Max: 19
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'enterprise/lib/captain/agent.rb'
|
- 'enterprise/lib/captain/agent.rb'
|
||||||
|
- 'enterprise/app/services/captain/llm/system_prompts_service.rb'
|
||||||
|
|
||||||
RSpec/ExampleLength:
|
RSpec/ExampleLength:
|
||||||
Max: 50
|
Max: 50
|
||||||
@ -230,6 +237,7 @@ AllCops:
|
|||||||
- 'tmp/**/*'
|
- 'tmp/**/*'
|
||||||
- 'storage/**/*'
|
- 'storage/**/*'
|
||||||
- 'db/migrate/20230426130150_init_schema.rb'
|
- 'db/migrate/20230426130150_init_schema.rb'
|
||||||
|
- 'reference_chatwoot/**/*'
|
||||||
|
|
||||||
FactoryBot/SyntaxMethods:
|
FactoryBot/SyntaxMethods:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|||||||
331
.rubocop_todo.yml
Normal file
331
.rubocop_todo.yml
Normal file
@ -0,0 +1,331 @@
|
|||||||
|
# This configuration was generated by
|
||||||
|
# `rubocop --auto-gen-config --exclude-limit 10000`
|
||||||
|
# on 2026-01-25 12:20:43 UTC using RuboCop version 1.75.6.
|
||||||
|
# The point is for the user to remove these configuration records
|
||||||
|
# one by one as the offenses are removed from the code base.
|
||||||
|
# Note that changes in the inspected code, or installation of new
|
||||||
|
# versions of RuboCop, may require this file to be generated again.
|
||||||
|
|
||||||
|
# Offense count: 2
|
||||||
|
Chatwoot/AttachmentDownload:
|
||||||
|
Exclude:
|
||||||
|
- 'app/services/whatsapp/providers/wuzapi_service.rb'
|
||||||
|
- 'debug_media.rb'
|
||||||
|
|
||||||
|
# Offense count: 32
|
||||||
|
# This cop supports safe autocorrection (--autocorrect).
|
||||||
|
# Configuration parameters: Max, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings.
|
||||||
|
# URISchemes: http, https
|
||||||
|
Layout/LineLength:
|
||||||
|
Exclude:
|
||||||
|
- 'app/jobs/conversations/cluster_job.rb'
|
||||||
|
- 'app/models/channel/whatsapp.rb'
|
||||||
|
- 'app/services/captain/reservations/sync_service.rb'
|
||||||
|
- 'app/services/whatsapp/providers/wuzapi_service.rb'
|
||||||
|
- 'enterprise/app/jobs/captain/conversation/debounce_response_job.rb'
|
||||||
|
- 'enterprise/app/services/captain/llm/system_prompts_service.rb'
|
||||||
|
- 'enterprise/app/services/captain/tools/check_availability_tool.rb'
|
||||||
|
- 'enterprise/app/services/captain/tools/create_reservation_intent_tool.rb'
|
||||||
|
- 'enterprise/app/services/captain/tools/generate_pix_tool.rb'
|
||||||
|
- 'enterprise/app/services/captain/tools/status_suites_tool.rb'
|
||||||
|
- 'enterprise/app/services/enterprise/message_templates/hook_execution_service.rb'
|
||||||
|
- 'enterprise/lib/captain/tools/scenario_delegator_tool.rb'
|
||||||
|
- 'lib/wuzapi/client.rb'
|
||||||
|
- 'scripts/debug_faq_search.rb'
|
||||||
|
- 'spec/factories/contacts.rb'
|
||||||
|
|
||||||
|
# Offense count: 4
|
||||||
|
# Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches, IgnoreDuplicateElseBranch.
|
||||||
|
Lint/DuplicateBranch:
|
||||||
|
Exclude:
|
||||||
|
- 'app/services/whatsapp/providers/wuzapi/payload_parser.rb'
|
||||||
|
- 'app/services/whatsapp/providers/wuzapi_service.rb'
|
||||||
|
- 'enterprise/app/services/captain/assistant/agent_runner_service.rb'
|
||||||
|
|
||||||
|
# Offense count: 2
|
||||||
|
Lint/DuplicateMethods:
|
||||||
|
Exclude:
|
||||||
|
- 'app/services/jasmine/semantic_search_service.rb'
|
||||||
|
|
||||||
|
# Offense count: 1
|
||||||
|
# Configuration parameters: MaximumRangeSize.
|
||||||
|
Lint/MissingCopEnableDirective:
|
||||||
|
Exclude:
|
||||||
|
- 'db/migrate/20260114100000_create_captain_inbox_automations.rb'
|
||||||
|
|
||||||
|
# Offense count: 1
|
||||||
|
Lint/NonLocalExitFromIterator:
|
||||||
|
Exclude:
|
||||||
|
- 'app/services/whatsapp/incoming_message_service_helpers.rb'
|
||||||
|
|
||||||
|
# Offense count: 1
|
||||||
|
Lint/ShadowedException:
|
||||||
|
Exclude:
|
||||||
|
- 'enterprise/app/services/captain/llm/paginated_faq_generator_service.rb'
|
||||||
|
|
||||||
|
# Offense count: 1
|
||||||
|
# This cop supports safe autocorrection (--autocorrect).
|
||||||
|
# Configuration parameters: AutoCorrect, AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods, NotImplementedExceptions.
|
||||||
|
# NotImplementedExceptions: NotImplementedError
|
||||||
|
Lint/UnusedMethodArgument:
|
||||||
|
Exclude:
|
||||||
|
- 'enterprise/app/services/llm/base_ai_service.rb'
|
||||||
|
|
||||||
|
# Offense count: 63
|
||||||
|
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
|
||||||
|
Metrics/AbcSize:
|
||||||
|
Exclude:
|
||||||
|
- 'app/jobs/conversations/auto_label_job.rb'
|
||||||
|
- 'app/jobs/conversations/resolution_job.rb'
|
||||||
|
- 'app/jobs/webhooks/whatsapp_events_job.rb'
|
||||||
|
- 'app/listeners/jasmine_listener.rb'
|
||||||
|
- 'app/models/channel/whatsapp.rb'
|
||||||
|
- 'app/models/message.rb'
|
||||||
|
- 'app/services/captain/reservations/sync_service.rb'
|
||||||
|
- 'app/services/crm_insights/update_service.rb'
|
||||||
|
- 'app/services/jasmine/brain_service.rb'
|
||||||
|
- 'app/services/jasmine/semantic_search_service.rb'
|
||||||
|
- 'app/services/jasmine/tool_runner.rb'
|
||||||
|
- 'app/services/whatsapp/decryption_service.rb'
|
||||||
|
- 'app/services/whatsapp/incoming_message_wuzapi_service.rb'
|
||||||
|
- 'app/services/whatsapp/providers/wuzapi/payload_parser.rb'
|
||||||
|
- 'app/services/whatsapp/providers/wuzapi_service.rb'
|
||||||
|
- 'config/initializers/fix_null_message_crash.rb'
|
||||||
|
- 'db/migrate/20251227054034_create_jasmine_tables.rb'
|
||||||
|
- 'db/migrate/20260110193000_fix_status_suites_headers.rb'
|
||||||
|
- 'debug_token.rb'
|
||||||
|
- 'enterprise/app/controllers/api/v1/accounts/captain/assistants_controller.rb'
|
||||||
|
- 'enterprise/app/controllers/api/v1/accounts/captain/reservations_controller.rb'
|
||||||
|
- 'enterprise/app/controllers/api/v1/accounts/captain/tools_controller.rb'
|
||||||
|
- 'enterprise/app/controllers/public/api/v1/captain/reservations_controller.rb'
|
||||||
|
- 'enterprise/app/controllers/public/api/v1/captain/webhooks_controller.rb'
|
||||||
|
- 'enterprise/app/jobs/captain/conversation/response_builder_job.rb'
|
||||||
|
- 'enterprise/app/models/captain/assistant.rb'
|
||||||
|
- 'enterprise/app/services/captain/assistant/agent_runner_service.rb'
|
||||||
|
- 'enterprise/app/services/captain/inter/auth_service.rb'
|
||||||
|
- 'enterprise/app/services/captain/inter/cob_service.rb'
|
||||||
|
- 'enterprise/app/services/captain/llm/assistant_chat_service.rb'
|
||||||
|
- 'enterprise/app/services/captain/llm/jasmine_brain.rb'
|
||||||
|
- 'enterprise/app/services/captain/reminders/create_service.rb'
|
||||||
|
- 'enterprise/app/services/captain/reservations/create_service.rb'
|
||||||
|
- 'enterprise/app/services/captain/tools/check_availability_tool.rb'
|
||||||
|
- 'enterprise/app/services/captain/tools/create_reservation_intent_tool.rb'
|
||||||
|
- 'enterprise/app/services/captain/tools/generate_pix_tool.rb'
|
||||||
|
- 'enterprise/app/services/captain/tools/react_to_message_tool.rb'
|
||||||
|
- 'enterprise/app/services/captain/tools/status_suites_tool.rb'
|
||||||
|
- 'enterprise/app/services/captain/tools/suite_watchdog_tool.rb'
|
||||||
|
- 'enterprise/app/services/captain/tools/update_contact_tool.rb'
|
||||||
|
- 'enterprise/lib/captain/tools/faq_lookup_tool.rb'
|
||||||
|
- 'enterprise/lib/captain/tools/http_tool.rb'
|
||||||
|
- 'enterprise/lib/captain/tools/scenario_delegator_tool.rb'
|
||||||
|
- 'lib/wuzapi/client.rb'
|
||||||
|
|
||||||
|
# Offense count: 6
|
||||||
|
# Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode.
|
||||||
|
# AllowedMethods: refine
|
||||||
|
Metrics/BlockLength:
|
||||||
|
Exclude:
|
||||||
|
- 'db/migrate/20260110193000_fix_status_suites_headers.rb'
|
||||||
|
- 'enterprise/app/controllers/public/api/v1/captain/reservations_controller.rb'
|
||||||
|
- 'enterprise/app/models/concerns/captain_tools_helpers.rb'
|
||||||
|
- 'script/test_auto_resolve_inbox.rb'
|
||||||
|
- 'seed_captain_tools.rb'
|
||||||
|
- 'seed_jasmine_hotel_v2.rb'
|
||||||
|
|
||||||
|
# Offense count: 11
|
||||||
|
# Configuration parameters: CountComments, Max, CountAsOne.
|
||||||
|
Metrics/ClassLength:
|
||||||
|
Exclude:
|
||||||
|
- 'app/models/channel/whatsapp.rb'
|
||||||
|
- 'app/services/crm_insights/update_service.rb'
|
||||||
|
- 'app/services/whatsapp/incoming_message_wuzapi_service.rb'
|
||||||
|
- 'enterprise/app/jobs/captain/conversation/response_builder_job.rb'
|
||||||
|
- 'enterprise/app/services/captain/assistant/agent_runner_service.rb'
|
||||||
|
- 'enterprise/app/services/captain/llm/assistant_chat_service.rb'
|
||||||
|
- 'enterprise/app/services/captain/llm/jasmine_brain.rb'
|
||||||
|
- 'enterprise/app/services/captain/tools/check_availability_tool.rb'
|
||||||
|
- 'enterprise/app/services/captain/tools/create_reservation_intent_tool.rb'
|
||||||
|
- 'enterprise/app/services/captain/tools/tool_runner.rb'
|
||||||
|
- 'enterprise/lib/captain/tools/scenario_delegator_tool.rb'
|
||||||
|
|
||||||
|
# Offense count: 67
|
||||||
|
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
||||||
|
Metrics/CyclomaticComplexity:
|
||||||
|
Max: 39
|
||||||
|
|
||||||
|
# Offense count: 78
|
||||||
|
# Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns.
|
||||||
|
Metrics/MethodLength:
|
||||||
|
Exclude:
|
||||||
|
- 'app/jobs/conversations/auto_label_job.rb'
|
||||||
|
- 'app/jobs/conversations/cluster_job.rb'
|
||||||
|
- 'app/jobs/jasmine/response_job.rb'
|
||||||
|
- 'app/listeners/jasmine_listener.rb'
|
||||||
|
- 'app/models/channel/whatsapp.rb'
|
||||||
|
- 'app/services/captain/inter_service.rb'
|
||||||
|
- 'app/services/captain/reservations/sync_service.rb'
|
||||||
|
- 'app/services/crm_insights/update_service.rb'
|
||||||
|
- 'app/services/jasmine/brain_service.rb'
|
||||||
|
- 'app/services/jasmine/embedding_service.rb'
|
||||||
|
- 'app/services/jasmine/semantic_search_service.rb'
|
||||||
|
- 'app/services/jasmine/tool_runner.rb'
|
||||||
|
- 'app/services/jasmine/vision_service.rb'
|
||||||
|
- 'app/services/whatsapp/decryption_service.rb'
|
||||||
|
- 'app/services/whatsapp/incoming_message_service_helpers.rb'
|
||||||
|
- 'app/services/whatsapp/incoming_message_wuzapi_service.rb'
|
||||||
|
- 'app/services/whatsapp/providers/wuzapi/payload_parser.rb'
|
||||||
|
- 'app/services/whatsapp/providers/wuzapi_service.rb'
|
||||||
|
- 'db/migrate/20251227054034_create_jasmine_tables.rb'
|
||||||
|
- 'db/migrate/20260104150000_add_crm_insights_history_fields.rb'
|
||||||
|
- 'db/migrate/20260110193000_fix_status_suites_headers.rb'
|
||||||
|
- 'db/migrate/20260114090200_create_captain_reminders.rb'
|
||||||
|
- 'debug_token.rb'
|
||||||
|
- 'enterprise/app/controllers/api/v1/accounts/captain/assistants_controller.rb'
|
||||||
|
- 'enterprise/app/controllers/api/v1/accounts/captain/scenarios_controller.rb'
|
||||||
|
- 'enterprise/app/controllers/public/api/v1/captain/reservations_controller.rb'
|
||||||
|
- 'enterprise/app/controllers/public/api/v1/captain/webhooks_controller.rb'
|
||||||
|
- 'enterprise/app/jobs/captain/conversation/response_builder_job.rb'
|
||||||
|
- 'enterprise/app/jobs/captain/intent_classification_job.rb'
|
||||||
|
- 'enterprise/app/services/captain/assistant/agent_runner_service.rb'
|
||||||
|
- 'enterprise/app/services/captain/llm/assistant_chat_service.rb'
|
||||||
|
- 'enterprise/app/services/captain/llm/jasmine_brain.rb'
|
||||||
|
- 'enterprise/app/services/captain/llm/system_prompts_service.rb'
|
||||||
|
- 'enterprise/app/services/captain/reminders/create_service.rb'
|
||||||
|
- 'enterprise/app/services/captain/reservations/create_service.rb'
|
||||||
|
- 'enterprise/app/services/captain/tools/check_availability_tool.rb'
|
||||||
|
- 'enterprise/app/services/captain/tools/create_reservation_intent_tool.rb'
|
||||||
|
- 'enterprise/app/services/captain/tools/generate_pix_tool.rb'
|
||||||
|
- 'enterprise/app/services/captain/tools/react_to_message_tool.rb'
|
||||||
|
- 'enterprise/app/services/captain/tools/reminder_tool.rb'
|
||||||
|
- 'enterprise/app/services/captain/tools/suite_watchdog_tool.rb'
|
||||||
|
- 'enterprise/app/services/captain/tools/update_contact_tool.rb'
|
||||||
|
- 'enterprise/app/services/captain/webhook_sender_service.rb'
|
||||||
|
- 'enterprise/app/services/captain/whatsapp_notification_service.rb'
|
||||||
|
- 'enterprise/lib/captain/tools/faq_lookup_tool.rb'
|
||||||
|
- 'enterprise/lib/captain/tools/scenario_delegator_tool.rb'
|
||||||
|
- 'lib/wuzapi/client.rb'
|
||||||
|
- 'scripts/force_create_captain_tables.rb'
|
||||||
|
|
||||||
|
# Offense count: 2
|
||||||
|
# Configuration parameters: CountComments, CountAsOne.
|
||||||
|
Metrics/ModuleLength:
|
||||||
|
Max: 132
|
||||||
|
|
||||||
|
# Offense count: 4
|
||||||
|
# Configuration parameters: CountKeywordArgs, MaxOptionalParameters.
|
||||||
|
Metrics/ParameterLists:
|
||||||
|
Max: 7
|
||||||
|
|
||||||
|
# Offense count: 46
|
||||||
|
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
||||||
|
Metrics/PerceivedComplexity:
|
||||||
|
Max: 40
|
||||||
|
|
||||||
|
# Offense count: 2
|
||||||
|
Naming/AccessorMethodName:
|
||||||
|
Exclude:
|
||||||
|
- 'app/services/captain/inter_service.rb'
|
||||||
|
- 'app/services/jasmine/vision_service.rb'
|
||||||
|
|
||||||
|
# Offense count: 7
|
||||||
|
# Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames.
|
||||||
|
# AllowedNames: as, at, by, cc, db, id, if, in, io, ip, of, on, os, pp, to
|
||||||
|
Naming/MethodParameterName:
|
||||||
|
Exclude:
|
||||||
|
- 'app/services/whatsapp/decryption_service.rb'
|
||||||
|
- 'enterprise/app/controllers/public/api/v1/captain/reservations_controller.rb'
|
||||||
|
- 'enterprise/app/services/captain/tools/create_reservation_intent_tool.rb'
|
||||||
|
|
||||||
|
# Offense count: 2
|
||||||
|
# Configuration parameters: MinSize.
|
||||||
|
Performance/CollectionLiteralInLoop:
|
||||||
|
Exclude:
|
||||||
|
- 'db/migrate/20260110193000_fix_status_suites_headers.rb'
|
||||||
|
|
||||||
|
# Offense count: 1
|
||||||
|
Rails/AfterCommitOverride:
|
||||||
|
Exclude:
|
||||||
|
- 'app/models/channel/whatsapp.rb'
|
||||||
|
|
||||||
|
# Offense count: 5
|
||||||
|
# Configuration parameters: Include.
|
||||||
|
# Include: **/app/models/**/*.rb
|
||||||
|
Rails/HasManyOrHasOneDependent:
|
||||||
|
Exclude:
|
||||||
|
- 'enterprise/app/models/captain/brand.rb'
|
||||||
|
- 'enterprise/app/models/captain/unit.rb'
|
||||||
|
|
||||||
|
# Offense count: 17
|
||||||
|
# Configuration parameters: IgnoreScopes, Include.
|
||||||
|
# Include: **/app/models/**/*.rb
|
||||||
|
Rails/InverseOf:
|
||||||
|
Exclude:
|
||||||
|
- 'app/models/captain/unit.rb'
|
||||||
|
- 'app/models/captain_assistant.rb'
|
||||||
|
- 'enterprise/app/models/captain/brand.rb'
|
||||||
|
- 'enterprise/app/models/captain/pricing.rb'
|
||||||
|
- 'enterprise/app/models/captain/pricing_inbox.rb'
|
||||||
|
- 'enterprise/app/models/captain/reservation.rb'
|
||||||
|
- 'enterprise/app/models/captain/unit.rb'
|
||||||
|
- 'enterprise/app/models/captain_inbox.rb'
|
||||||
|
|
||||||
|
# Offense count: 9
|
||||||
|
# Configuration parameters: ForbiddenMethods, AllowedMethods.
|
||||||
|
# ForbiddenMethods: decrement!, decrement_counter, increment!, increment_counter, insert, insert!, insert_all, insert_all!, toggle!, touch, touch_all, update_all, update_attribute, update_column, update_columns, update_counters, upsert, upsert_all
|
||||||
|
Rails/SkipsModelValidations:
|
||||||
|
Exclude:
|
||||||
|
- 'app/services/jasmine/tool_runner.rb'
|
||||||
|
- 'enterprise/app/jobs/captain/documents/response_builder_job.rb'
|
||||||
|
- 'enterprise/app/services/captain/tools/create_reservation_intent_tool.rb'
|
||||||
|
- 'enterprise/app/services/captain/tools/generate_pix_tool.rb'
|
||||||
|
- 'promote_super_admin.rb'
|
||||||
|
- 'script/test_auto_resolve_inbox.rb'
|
||||||
|
|
||||||
|
# Offense count: 8
|
||||||
|
# Configuration parameters: Include.
|
||||||
|
# Include: db/**/*.rb
|
||||||
|
Rails/ThreeStateBooleanColumn:
|
||||||
|
Exclude:
|
||||||
|
- 'db/migrate/20240523120000_create_captain_configurations.rb'
|
||||||
|
- 'db/migrate/20251227054034_create_jasmine_tables.rb'
|
||||||
|
- 'db/migrate/20251227201733_create_captain_tool_configs.rb'
|
||||||
|
- 'db/migrate/20260114101004_create_captain_extras.rb'
|
||||||
|
- 'db/migrate/20260120141736_add_message_signature_enabled_to_inboxes.rb'
|
||||||
|
- 'db/migrate/20260121165034_add_plug_play_to_captain_units.rb'
|
||||||
|
|
||||||
|
# Offense count: 2
|
||||||
|
# Configuration parameters: TransactionMethods.
|
||||||
|
Rails/TransactionExitStatement:
|
||||||
|
Exclude:
|
||||||
|
- 'app/services/jasmine/embedding_service.rb'
|
||||||
|
- 'app/services/whatsapp/incoming_message_wuzapi_service.rb'
|
||||||
|
|
||||||
|
# Offense count: 1
|
||||||
|
# Configuration parameters: Include.
|
||||||
|
# Include: **/app/models/**/*.rb
|
||||||
|
Rails/UniqueValidationWithoutIndex:
|
||||||
|
Exclude:
|
||||||
|
- 'app/models/jasmine/inbox_collection.rb'
|
||||||
|
|
||||||
|
# Offense count: 3
|
||||||
|
# Configuration parameters: MinBranchesCount.
|
||||||
|
Style/HashLikeCase:
|
||||||
|
Exclude:
|
||||||
|
- 'app/services/whatsapp/providers/wuzapi/payload_parser.rb'
|
||||||
|
- 'enterprise/app/jobs/captain/conversation/response_builder_job.rb'
|
||||||
|
|
||||||
|
# Offense count: 4
|
||||||
|
Style/OneClassPerFile:
|
||||||
|
Exclude:
|
||||||
|
- 'app/services/jasmine/brain_service.rb'
|
||||||
|
|
||||||
|
# Offense count: 8
|
||||||
|
UseFromEmail:
|
||||||
|
Exclude:
|
||||||
|
- 'app/services/captain/reservations/sync_service.rb'
|
||||||
|
- 'clear_chat_history.rb'
|
||||||
|
- 'debug_auth.rb'
|
||||||
|
- 'debug_token.rb'
|
||||||
|
- 'force_reset.rb'
|
||||||
|
- 'promote_super_admin.rb'
|
||||||
|
- 'setup_docker_env.rb'
|
||||||
@ -26,18 +26,10 @@ class Api::V1::Accounts::Inboxes::WuzapiController < Api::V1::Accounts::BaseCont
|
|||||||
status = status_data['status'] || status_data['state'] || status_data
|
status = status_data['status'] || status_data['state'] || status_data
|
||||||
Rails.logger.info "Wuzapi Connect/QR Flow - Current Status: #{status}"
|
Rails.logger.info "Wuzapi Connect/QR Flow - Current Status: #{status}"
|
||||||
|
|
||||||
if %w[CONNECTED inChat success].include?(status)
|
return if already_connected?(status)
|
||||||
Rails.logger.info 'Wuzapi is already connected. Skipping QR.'
|
|
||||||
render json: { qrcode: nil, status: 'CONNECTED', message: 'Already connected' }
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
qr_data = client.get_qr_code(user_token)
|
qr_data = client.get_qr_code(user_token)
|
||||||
Rails.logger.info "Wuzapi QR Data Response keys: #{begin
|
log_qr_data_keys(qr_data)
|
||||||
qr_data.keys
|
|
||||||
rescue StandardError
|
|
||||||
'nil'
|
|
||||||
end}"
|
|
||||||
render json: qr_data
|
render json: qr_data
|
||||||
rescue Wuzapi::Client::Error => e
|
rescue Wuzapi::Client::Error => e
|
||||||
Rails.logger.error "Wuzapi QR Error: #{e.message}"
|
Rails.logger.error "Wuzapi QR Error: #{e.message}"
|
||||||
@ -118,8 +110,24 @@ class Api::V1::Accounts::Inboxes::WuzapiController < Api::V1::Accounts::BaseCont
|
|||||||
Rails.logger.error "Wuzapi Token Missing for Inbox #{@inbox.id}"
|
Rails.logger.error "Wuzapi Token Missing for Inbox #{@inbox.id}"
|
||||||
raise 'Token Wuzapi ausente; reprovisionar usuário'
|
raise 'Token Wuzapi ausente; reprovisionar usuário'
|
||||||
else
|
else
|
||||||
Rails.logger.info "Wuzapi Request using Token (last 6): ******#{token.to_s[-6..-1]}"
|
Rails.logger.info "Wuzapi Request using Token (last 6): ******#{token.to_s[-6..]}"
|
||||||
end
|
end
|
||||||
token
|
token
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def already_connected?(status)
|
||||||
|
if %w[CONNECTED inChat success].include?(status)
|
||||||
|
Rails.logger.info 'Wuzapi is already connected. Skipping QR.'
|
||||||
|
render json: { qrcode: nil, status: 'CONNECTED', message: 'Already connected' }
|
||||||
|
true
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def log_qr_data_keys(qr_data)
|
||||||
|
Rails.logger.info "Wuzapi QR Data Response keys: #{qr_data.keys}"
|
||||||
|
rescue StandardError
|
||||||
|
Rails.logger.info 'Wuzapi QR Data Response keys: nil'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -54,6 +54,6 @@ class Api::V1::Accounts::Integrations::LlmModelsController < Api::V1::Accounts::
|
|||||||
Rails.logger.error(
|
Rails.logger.error(
|
||||||
"[LLM][ModelTest] Failed to persist model test results hook_id=#{hook.id} errors=#{hook.errors.full_messages.join(', ')}"
|
"[LLM][ModelTest] Failed to persist model test results hook_id=#{hook.id} errors=#{hook.errors.full_messages.join(', ')}"
|
||||||
)
|
)
|
||||||
hook.update_columns(settings: settings)
|
hook.update_columns(settings: settings) # rubocop:disable Rails/SkipsModelValidations
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,8 +1,4 @@
|
|||||||
module Api
|
class Api::V1::Accounts::Jasmine::CollectionsController < Api::V1::Accounts::BaseController
|
||||||
module V1
|
|
||||||
module Accounts
|
|
||||||
module Jasmine
|
|
||||||
class CollectionsController < Api::V1::Accounts::BaseController
|
|
||||||
before_action :find_collection, only: [:destroy]
|
before_action :find_collection, only: [:destroy]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@ -44,8 +40,3 @@ module Api
|
|||||||
params.require(:collection).permit(:name, :description, :visibility, :owner_inbox_id)
|
params.require(:collection).permit(:name, :description, :visibility, :owner_inbox_id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,4 @@
|
|||||||
module Api
|
class Api::V1::Accounts::Jasmine::DocumentsController < Api::V1::Accounts::BaseController
|
||||||
module V1
|
|
||||||
module Accounts
|
|
||||||
module Jasmine
|
|
||||||
class DocumentsController < Api::V1::Accounts::BaseController
|
|
||||||
before_action :fetch_collection
|
before_action :fetch_collection
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@ -36,7 +32,3 @@ module Api
|
|||||||
params.require(:document).permit(:title, :content)
|
params.require(:document).permit(:title, :content)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|||||||
@ -1,8 +1,4 @@
|
|||||||
module Api
|
class Api::V1::Accounts::Jasmine::InboxCollectionsController < Api::V1::Accounts::BaseController
|
||||||
module V1
|
|
||||||
module Accounts
|
|
||||||
module Jasmine
|
|
||||||
class InboxCollectionsController < Api::V1::Accounts::BaseController
|
|
||||||
before_action :fetch_inbox
|
before_action :fetch_inbox
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@ -37,7 +33,3 @@ module Api
|
|||||||
@inbox = Current.account.inboxes.find(params[:inbox_id])
|
@inbox = Current.account.inboxes.find(params[:inbox_id])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|||||||
@ -1,8 +1,4 @@
|
|||||||
module Api
|
class Api::V1::Accounts::Jasmine::InboxConfigsController < Api::V1::Accounts::BaseController
|
||||||
module V1
|
|
||||||
module Accounts
|
|
||||||
module Jasmine
|
|
||||||
class InboxConfigsController < Api::V1::Accounts::BaseController
|
|
||||||
before_action :fetch_inbox
|
before_action :fetch_inbox
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@ -30,7 +26,3 @@ module Api
|
|||||||
params.require(:inbox_config).permit(:name, :system_prompt, :is_enabled, :mode)
|
params.require(:inbox_config).permit(:name, :system_prompt, :is_enabled, :mode)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|||||||
@ -1,8 +1,4 @@
|
|||||||
module Public
|
class Public::Api::V1::Captain::PaymentsController < ApplicationController
|
||||||
module Api
|
|
||||||
module V1
|
|
||||||
module Captain
|
|
||||||
class PaymentsController < ApplicationController
|
|
||||||
layout false
|
layout false
|
||||||
skip_before_action :authenticate_user!, raise: false
|
skip_before_action :authenticate_user!, raise: false
|
||||||
skip_before_action :check_current_user_is_active, raise: false
|
skip_before_action :check_current_user_is_active, raise: false
|
||||||
@ -19,7 +15,3 @@ module Public
|
|||||||
# It should be a Captain::PixCharge model
|
# It should be a Captain::PixCharge model
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|||||||
@ -37,9 +37,22 @@ class Webhooks::WhatsappController < ActionController::API
|
|||||||
# This prevents ANY binary data from leaking into JSON serialization
|
# This prevents ANY binary data from leaking into JSON serialization
|
||||||
def sanitize_payload_for_sidekiq
|
def sanitize_payload_for_sidekiq
|
||||||
raw = params.to_unsafe_hash
|
raw = params.to_unsafe_hash
|
||||||
|
clean_payload = build_base_payload(raw)
|
||||||
|
|
||||||
# Build a completely NEW payload with only safe fields
|
clean_payload['event'] = build_clean_event(raw['event']) if raw['event'].is_a?(Hash)
|
||||||
clean_payload = {
|
|
||||||
|
if raw['whatsapp'].is_a?(Hash)
|
||||||
|
clean_payload['whatsapp'] = { 'event' => clean_payload['event'] }.merge(
|
||||||
|
raw['whatsapp'].slice('type', 'state', 'instanceName', 'userID')
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
Rails.logger.info 'WuzAPI: Payload sanitized (WHITELIST mode)'
|
||||||
|
deep_force_utf8(clean_payload)
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_base_payload(raw)
|
||||||
|
{
|
||||||
'type' => raw['type'],
|
'type' => raw['type'],
|
||||||
'state' => raw['state'],
|
'state' => raw['state'],
|
||||||
'instanceName' => raw['instanceName'],
|
'instanceName' => raw['instanceName'],
|
||||||
@ -48,14 +61,13 @@ class Webhooks::WhatsappController < ActionController::API
|
|||||||
'action' => raw['action'],
|
'action' => raw['action'],
|
||||||
'phone_number' => raw['phone_number']
|
'phone_number' => raw['phone_number']
|
||||||
}
|
}
|
||||||
|
end
|
||||||
|
|
||||||
# Only copy safe event fields
|
def build_clean_event(raw_event)
|
||||||
if raw['event'].is_a?(Hash)
|
|
||||||
clean_event = {}
|
clean_event = {}
|
||||||
|
|
||||||
# Info fields (all safe strings/ids)
|
if raw_event['Info'].is_a?(Hash)
|
||||||
if raw['event']['Info'].is_a?(Hash)
|
clean_event['Info'] = raw_event['Info'].slice(
|
||||||
clean_event['Info'] = raw['event']['Info'].slice(
|
|
||||||
'ID', 'Type', 'MediaType', 'Chat', 'Sender', 'SenderAlt', 'RecipientAlt', 'IsFromMe', 'IsGroup',
|
'ID', 'Type', 'MediaType', 'Chat', 'Sender', 'SenderAlt', 'RecipientAlt', 'IsFromMe', 'IsGroup',
|
||||||
'Timestamp', 'PushName', 'MessageSource'
|
'Timestamp', 'PushName', 'MessageSource'
|
||||||
)
|
)
|
||||||
@ -64,98 +76,76 @@ class Webhooks::WhatsappController < ActionController::API
|
|||||||
# Safe event metadata
|
# Safe event metadata
|
||||||
%w[Chat Sender IsFromMe IsGroup Timestamp AddressingMode BroadcastListOwner
|
%w[Chat Sender IsFromMe IsGroup Timestamp AddressingMode BroadcastListOwner
|
||||||
BroadcastRecipients RecipientAlt SenderAlt MessageIDs MessageSender].each do |key|
|
BroadcastRecipients RecipientAlt SenderAlt MessageIDs MessageSender].each do |key|
|
||||||
clean_event[key] = raw['event'][key] if raw['event'].key?(key)
|
clean_event[key] = raw_event[key] if raw_event.key?(key)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Message content - WHITELIST only safe fields
|
if raw_event['Message'].is_a?(Hash)
|
||||||
if raw['event']['Message'].is_a?(Hash)
|
clean_msg = build_clean_message(raw_event['Message'])
|
||||||
msg = raw['event']['Message']
|
clean_event['Message'] = clean_msg unless clean_msg.empty?
|
||||||
clean_msg = {}
|
end
|
||||||
|
|
||||||
# Text messages
|
clean_event
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_clean_message(msg)
|
||||||
|
clean_msg = {}
|
||||||
clean_msg['conversation'] = msg['conversation'] if msg['conversation'].is_a?(String)
|
clean_msg['conversation'] = msg['conversation'] if msg['conversation'].is_a?(String)
|
||||||
|
|
||||||
if msg['extendedTextMessage'].is_a?(Hash)
|
clean_msg.merge!(clean_extended_text_message(msg['extendedTextMessage']))
|
||||||
clean_msg['extendedTextMessage'] = {
|
clean_msg.merge!(clean_media_message(msg, 'imageMessage'))
|
||||||
'text' => msg['extendedTextMessage']['text']
|
clean_msg.merge!(clean_media_message(msg, 'videoMessage'))
|
||||||
}
|
clean_msg.merge!(clean_media_message(msg, 'audioMessage'))
|
||||||
# Only copy contextInfo if it doesn't have quotedMessage with binaries
|
clean_msg.merge!(clean_document_message(msg['documentMessage']))
|
||||||
# Only copy contextInfo if it doesn't have quotedMessage with binaries
|
|
||||||
if msg['extendedTextMessage']['contextInfo'].is_a?(Hash)
|
clean_msg
|
||||||
ctx = msg['extendedTextMessage']['contextInfo']
|
end
|
||||||
clean_msg['extendedTextMessage']['contextInfo'] = {
|
|
||||||
|
def clean_extended_text_message(ext_msg)
|
||||||
|
return {} unless ext_msg.is_a?(Hash)
|
||||||
|
|
||||||
|
result = { 'extendedTextMessage' => { 'text' => ext_msg['text'] } }
|
||||||
|
|
||||||
|
result['extendedTextMessage']['contextInfo'] = clean_context_info(ext_msg['contextInfo']) if ext_msg['contextInfo'].is_a?(Hash)
|
||||||
|
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
|
def clean_context_info(ctx)
|
||||||
|
{
|
||||||
'stanzaID' => ctx['stanzaID'] || ctx['stanzaId'],
|
'stanzaID' => ctx['stanzaID'] || ctx['stanzaId'],
|
||||||
'participant' => ctx['participant']
|
'participant' => ctx['participant']
|
||||||
}.compact
|
}.compact
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def clean_media_message(msg, type)
|
||||||
|
media = msg[type]
|
||||||
|
return {} unless media.is_a?(Hash)
|
||||||
|
|
||||||
|
clean_data = {
|
||||||
|
'URL' => media['URL'] || media['url'],
|
||||||
|
'directPath' => media['directPath'],
|
||||||
|
'mediaKey' => media['mediaKey'],
|
||||||
|
'fileEncSha256' => media['fileEncSha256'] || media['fileEncSHA256'],
|
||||||
|
'fileSha256' => media['fileSha256'] || media['fileSHA256'],
|
||||||
|
'fileLength' => media['fileLength'],
|
||||||
|
'mimetype' => media['mimetype'],
|
||||||
|
'seconds' => media['seconds'],
|
||||||
|
'caption' => media['caption'],
|
||||||
|
'ptt' => media['ptt'],
|
||||||
|
'width' => media['width'],
|
||||||
|
'height' => media['height']
|
||||||
|
}
|
||||||
|
|
||||||
|
clean_data['contextInfo'] = clean_context_info(media['contextInfo']) if media['contextInfo'].is_a?(Hash)
|
||||||
|
|
||||||
|
{ type => clean_data.compact }
|
||||||
end
|
end
|
||||||
|
|
||||||
# Image messages - ONLY safe metadata, NO binaries
|
def clean_document_message(doc)
|
||||||
if msg['imageMessage'].is_a?(Hash)
|
return {} unless doc.is_a?(Hash)
|
||||||
img = msg['imageMessage']
|
|
||||||
clean_msg['imageMessage'] = {
|
|
||||||
'URL' => img['URL'] || img['url'],
|
|
||||||
'directPath' => img['directPath'],
|
|
||||||
'mediaKey' => img['mediaKey'],
|
|
||||||
'fileEncSha256' => img['fileEncSha256'] || img['fileEncSHA256'],
|
|
||||||
'fileSha256' => img['fileSha256'] || img['fileSHA256'],
|
|
||||||
'fileLength' => img['fileLength'],
|
|
||||||
'mimetype' => img['mimetype'],
|
|
||||||
'width' => img['width'],
|
|
||||||
'height' => img['height'],
|
|
||||||
'caption' => img['caption'],
|
|
||||||
'contextInfo' => {
|
|
||||||
'stanzaID' => img.dig('contextInfo', 'stanzaID') || img.dig('contextInfo', 'stanzaId'),
|
|
||||||
'participant' => img.dig('contextInfo', 'participant')
|
|
||||||
}.compact
|
|
||||||
}.compact
|
|
||||||
# EXPLICITLY NO: JPEGThumbnail, scansSidecar, firstScanSidecar, etc
|
|
||||||
end
|
|
||||||
|
|
||||||
# Video messages - ONLY safe metadata
|
{
|
||||||
if msg['videoMessage'].is_a?(Hash)
|
'documentMessage' => {
|
||||||
vid = msg['videoMessage']
|
|
||||||
clean_msg['videoMessage'] = {
|
|
||||||
'URL' => vid['URL'] || vid['url'],
|
|
||||||
'directPath' => vid['directPath'],
|
|
||||||
'mediaKey' => vid['mediaKey'],
|
|
||||||
'fileEncSha256' => vid['fileEncSha256'] || vid['fileEncSHA256'],
|
|
||||||
'fileSha256' => vid['fileSha256'] || vid['fileSHA256'],
|
|
||||||
'fileLength' => vid['fileLength'],
|
|
||||||
'mimetype' => vid['mimetype'],
|
|
||||||
'seconds' => vid['seconds'],
|
|
||||||
'caption' => vid['caption'],
|
|
||||||
'contextInfo' => {
|
|
||||||
'stanzaID' => vid.dig('contextInfo', 'stanzaID') || vid.dig('contextInfo', 'stanzaId'),
|
|
||||||
'participant' => vid.dig('contextInfo', 'participant')
|
|
||||||
}.compact
|
|
||||||
}.compact
|
|
||||||
end
|
|
||||||
|
|
||||||
# Audio messages
|
|
||||||
if msg['audioMessage'].is_a?(Hash)
|
|
||||||
aud = msg['audioMessage']
|
|
||||||
clean_msg['audioMessage'] = {
|
|
||||||
'URL' => aud['URL'] || aud['url'],
|
|
||||||
'directPath' => aud['directPath'],
|
|
||||||
'mediaKey' => aud['mediaKey'],
|
|
||||||
'fileEncSha256' => aud['fileEncSha256'] || aud['fileEncSHA256'],
|
|
||||||
'fileSha256' => aud['fileSha256'] || aud['fileSHA256'],
|
|
||||||
'fileLength' => aud['fileLength'],
|
|
||||||
'mimetype' => aud['mimetype'],
|
|
||||||
'seconds' => aud['seconds'],
|
|
||||||
'ptt' => aud['ptt'],
|
|
||||||
'contextInfo' => {
|
|
||||||
'stanzaID' => aud.dig('contextInfo', 'stanzaID') || aud.dig('contextInfo', 'stanzaId'),
|
|
||||||
'participant' => aud.dig('contextInfo', 'participant')
|
|
||||||
}.compact
|
|
||||||
}.compact
|
|
||||||
end
|
|
||||||
|
|
||||||
# Document messages
|
|
||||||
if msg['documentMessage'].is_a?(Hash)
|
|
||||||
doc = msg['documentMessage']
|
|
||||||
clean_msg['documentMessage'] = {
|
|
||||||
'URL' => doc['URL'] || doc['url'],
|
'URL' => doc['URL'] || doc['url'],
|
||||||
'directPath' => doc['directPath'],
|
'directPath' => doc['directPath'],
|
||||||
'mediaKey' => doc['mediaKey'],
|
'mediaKey' => doc['mediaKey'],
|
||||||
@ -166,24 +156,7 @@ class Webhooks::WhatsappController < ActionController::API
|
|||||||
'fileName' => doc['fileName'],
|
'fileName' => doc['fileName'],
|
||||||
'title' => doc['title']
|
'title' => doc['title']
|
||||||
}.compact
|
}.compact
|
||||||
end
|
}
|
||||||
|
|
||||||
clean_event['Message'] = clean_msg unless clean_msg.empty?
|
|
||||||
end
|
|
||||||
|
|
||||||
clean_payload['event'] = clean_event
|
|
||||||
end
|
|
||||||
|
|
||||||
# Also copy whatsapp key if present (but sanitize it too)
|
|
||||||
if raw['whatsapp'].is_a?(Hash)
|
|
||||||
# Just reference the same clean event structure
|
|
||||||
clean_payload['whatsapp'] = { 'event' => clean_payload['event'] }.merge(
|
|
||||||
raw['whatsapp'].slice('type', 'state', 'instanceName', 'userID')
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
Rails.logger.info 'WuzAPI: Payload sanitized (WHITELIST mode)'
|
|
||||||
deep_force_utf8(clean_payload)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def deep_force_utf8(obj)
|
def deep_force_utf8(obj)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
class Webhooks::WuzapiController < ActionController::Base
|
class Webhooks::WuzapiController < ApplicationController
|
||||||
skip_before_action :verify_authenticity_token
|
skip_before_action :verify_authenticity_token
|
||||||
before_action :fetch_inbox
|
before_action :fetch_inbox
|
||||||
before_action :verify_secret
|
before_action :verify_secret
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
module Conversations
|
class Conversations::AutoLabelJob < ApplicationJob
|
||||||
class AutoLabelJob < ApplicationJob
|
|
||||||
queue_as :low_priority
|
queue_as :low_priority
|
||||||
|
|
||||||
TAXONOMY = {
|
TAXONOMY = {
|
||||||
@ -17,7 +16,7 @@ module Conversations
|
|||||||
def perform(conversation_id)
|
def perform(conversation_id)
|
||||||
conversation = Conversation.find_by(id: conversation_id)
|
conversation = Conversation.find_by(id: conversation_id)
|
||||||
return unless conversation
|
return unless conversation
|
||||||
return unless conversation.messages.count > 0
|
return unless conversation.messages.count.positive?
|
||||||
|
|
||||||
# Evita re-classificar se já tiver alguma label de IA (opcional)
|
# Evita re-classificar se já tiver alguma label de IA (opcional)
|
||||||
# return if (conversation.label_list & TAXONOMY.keys).any?
|
# return if (conversation.label_list & TAXONOMY.keys).any?
|
||||||
@ -131,4 +130,3 @@ module Conversations
|
|||||||
Rails.logger.info "[AutoLabelJob] Applied label #{label_name} and saved reason '#{question_summary}' to conversation #{conversation.id}"
|
Rails.logger.info "[AutoLabelJob] Applied label #{label_name} and saved reason '#{question_summary}' to conversation #{conversation.id}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
module Conversations
|
class Conversations::ClusterJob < ApplicationJob
|
||||||
class ClusterJob < ApplicationJob
|
|
||||||
queue_as :low_priority
|
queue_as :low_priority
|
||||||
|
|
||||||
def perform(account_id, days_back = 7)
|
def perform(account_id, days_back = 7)
|
||||||
@ -9,7 +8,7 @@ module Conversations
|
|||||||
start_date = days_back.days.ago.beginning_of_day
|
start_date = days_back.days.ago.beginning_of_day
|
||||||
|
|
||||||
# Labels que queremos agrupar (todas da taxonomia)
|
# Labels que queremos agrupar (todas da taxonomia)
|
||||||
Conversations::AutoLabelJob::TAXONOMY.keys.each do |label|
|
Conversations::AutoLabelJob::TAXONOMY.each_key do |label|
|
||||||
# Busca perguntas dessa categoria
|
# Busca perguntas dessa categoria
|
||||||
# Note: estamos queryng o campo JSONB additional_attributes
|
# Note: estamos queryng o campo JSONB additional_attributes
|
||||||
account.conversations
|
account.conversations
|
||||||
@ -83,4 +82,3 @@ module Conversations
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
module CrmInsights
|
class CrmInsights::UpdateJob < ApplicationJob
|
||||||
class UpdateJob < ApplicationJob
|
|
||||||
queue_as :low
|
queue_as :low
|
||||||
|
|
||||||
def perform(conversation_id, reason: nil)
|
def perform(conversation_id, reason: nil)
|
||||||
@ -14,4 +13,3 @@ module CrmInsights
|
|||||||
UpdateService.new(conversation: conversation, reason: reason).call
|
UpdateService.new(conversation: conversation, reason: reason).call
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
module Jasmine
|
class Jasmine::ResponseJob < ApplicationJob
|
||||||
class ResponseJob < ApplicationJob
|
|
||||||
queue_as :default
|
queue_as :default
|
||||||
|
|
||||||
retry_on StandardError, wait: :polynomially_longer, attempts: 2
|
retry_on StandardError, wait: :polynomially_longer, attempts: 2
|
||||||
@ -58,4 +57,3 @@ module Jasmine
|
|||||||
Rails.logger.error "[Jasmine::ResponseJob] Failed to send response: #{e.message}"
|
Rails.logger.error "[Jasmine::ResponseJob] Failed to send response: #{e.message}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|||||||
@ -25,10 +25,8 @@
|
|||||||
# fk_rails_... (account_id => accounts.id)
|
# fk_rails_... (account_id => accounts.id)
|
||||||
# fk_rails_... (captain_brand_id => captain_brands.id)
|
# fk_rails_... (captain_brand_id => captain_brands.id)
|
||||||
#
|
#
|
||||||
module Captain
|
class Captain::Pricing < ApplicationRecord
|
||||||
class Pricing < ApplicationRecord
|
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
belongs_to :captain_brand, optional: true
|
belongs_to :captain_brand, optional: true
|
||||||
belongs_to :inbox, optional: true
|
belongs_to :inbox, optional: true
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|||||||
@ -48,8 +48,7 @@
|
|||||||
# fk_rails_... (conversation_id => conversations.id)
|
# fk_rails_... (conversation_id => conversations.id)
|
||||||
# fk_rails_... (inbox_id => inboxes.id)
|
# fk_rails_... (inbox_id => inboxes.id)
|
||||||
#
|
#
|
||||||
module Captain
|
class Captain::Reservation < ApplicationRecord
|
||||||
class Reservation < ApplicationRecord
|
|
||||||
self.table_name = 'captain_reservations'
|
self.table_name = 'captain_reservations'
|
||||||
|
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
@ -83,4 +82,3 @@ module Captain
|
|||||||
where('check_in_at < ? AND check_out_at > ?', end_date, start_date)
|
where('check_in_at < ? AND check_out_at > ?', end_date, start_date)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|||||||
@ -38,8 +38,7 @@
|
|||||||
# fk_rails_... (captain_brand_id => captain_brands.id)
|
# fk_rails_... (captain_brand_id => captain_brands.id)
|
||||||
# fk_rails_... (inbox_id => inboxes.id)
|
# fk_rails_... (inbox_id => inboxes.id)
|
||||||
#
|
#
|
||||||
module Captain
|
class Captain::Unit < ApplicationRecord
|
||||||
class Unit < ApplicationRecord
|
|
||||||
self.table_name = 'captain_units'
|
self.table_name = 'captain_units'
|
||||||
|
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
@ -57,4 +56,3 @@ module Captain
|
|||||||
|
|
||||||
validates :name, presence: true
|
validates :name, presence: true
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|||||||
@ -197,7 +197,7 @@ class Channel::Whatsapp < ApplicationRecord
|
|||||||
provider_config.delete('wuzapi_user_token')
|
provider_config.delete('wuzapi_user_token')
|
||||||
end
|
end
|
||||||
|
|
||||||
return unless provider_config['wuzapi_admin_token'].present?
|
return if provider_config['wuzapi_admin_token'].blank?
|
||||||
|
|
||||||
self.wuzapi_admin_token = provider_config['wuzapi_admin_token']
|
self.wuzapi_admin_token = provider_config['wuzapi_admin_token']
|
||||||
provider_config.delete('wuzapi_admin_token')
|
provider_config.delete('wuzapi_admin_token')
|
||||||
@ -209,13 +209,13 @@ class Channel::Whatsapp < ApplicationRecord
|
|||||||
|
|
||||||
def perform_webhook_setup
|
def perform_webhook_setup
|
||||||
if provider == 'wuzapi'
|
if provider == 'wuzapi'
|
||||||
return unless inbox.present?
|
return if inbox.blank?
|
||||||
|
|
||||||
base_url = provider_config['wuzapi_base_url']
|
base_url = provider_config['wuzapi_base_url']
|
||||||
# Use encrypted token
|
# Use encrypted token
|
||||||
user_token = wuzapi_user_token
|
user_token = wuzapi_user_token
|
||||||
|
|
||||||
return unless user_token.present?
|
return if user_token.blank?
|
||||||
|
|
||||||
# Construct Chatwoot Webhook URL
|
# Construct Chatwoot Webhook URL
|
||||||
# Using standard route: /webhooks/whatsapp/:phone_number for WuzAPI as per fix
|
# Using standard route: /webhooks/whatsapp/:phone_number for WuzAPI as per fix
|
||||||
@ -286,7 +286,7 @@ class Channel::Whatsapp < ApplicationRecord
|
|||||||
provider_config['wuzapi_user_id'] = result[:wuzapi_user_id]
|
provider_config['wuzapi_user_id'] = result[:wuzapi_user_id]
|
||||||
self.wuzapi_user_token = result[:wuzapi_user_token]
|
self.wuzapi_user_token = result[:wuzapi_user_token]
|
||||||
|
|
||||||
masked_token = result[:wuzapi_user_token].to_s[-4..-1]
|
masked_token = result[:wuzapi_user_token].to_s[-4..]
|
||||||
Rails.logger.info "Wuzapi User Provisioned. ID: #{result[:wuzapi_user_id]}, Token (last 4): ****#{masked_token}"
|
Rails.logger.info "Wuzapi User Provisioned. ID: #{result[:wuzapi_user_id]}, Token (last 4): ****#{masked_token}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -24,15 +24,14 @@
|
|||||||
# fk_rails_... (account_id => accounts.id)
|
# fk_rails_... (account_id => accounts.id)
|
||||||
# fk_rails_... (owner_inbox_id => inboxes.id)
|
# fk_rails_... (owner_inbox_id => inboxes.id)
|
||||||
#
|
#
|
||||||
module Jasmine
|
class Jasmine::Collection < ApplicationRecord
|
||||||
class Collection < ApplicationRecord
|
|
||||||
self.table_name = 'jasmine_collections'
|
self.table_name = 'jasmine_collections'
|
||||||
|
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
belongs_to :owner_inbox, class_name: 'Inbox', optional: true
|
belongs_to :owner_inbox, class_name: 'Inbox', optional: true
|
||||||
|
|
||||||
has_many :inbox_collections, class_name: 'Jasmine::InboxCollection', foreign_key: :collection_id, dependent: :destroy
|
has_many :inbox_collections, class_name: 'Jasmine::InboxCollection', dependent: :destroy
|
||||||
has_many :documents, class_name: 'Jasmine::Document', foreign_key: :collection_id, dependent: :destroy
|
has_many :documents, class_name: 'Jasmine::Document', dependent: :destroy
|
||||||
|
|
||||||
enum visibility: { private: 0, shared: 1, global: 2 }, _prefix: true
|
enum visibility: { private: 0, shared: 1, global: 2 }, _prefix: true
|
||||||
|
|
||||||
@ -43,9 +42,8 @@ module Jasmine
|
|||||||
private
|
private
|
||||||
|
|
||||||
def validate_owner_if_private
|
def validate_owner_if_private
|
||||||
if visibility_private? && owner_inbox_id.nil?
|
return unless visibility_private? && owner_inbox_id.nil?
|
||||||
|
|
||||||
errors.add(:owner_inbox_id, 'must be present for private collections')
|
errors.add(:owner_inbox_id, 'must be present for private collections')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|||||||
@ -25,13 +25,12 @@
|
|||||||
# fk_rails_... (account_id => accounts.id)
|
# fk_rails_... (account_id => accounts.id)
|
||||||
# fk_rails_... (collection_id => jasmine_collections.id)
|
# fk_rails_... (collection_id => jasmine_collections.id)
|
||||||
#
|
#
|
||||||
module Jasmine
|
class Jasmine::Document < ApplicationRecord
|
||||||
class Document < ApplicationRecord
|
|
||||||
self.table_name = 'jasmine_documents'
|
self.table_name = 'jasmine_documents'
|
||||||
|
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
belongs_to :collection, class_name: 'Jasmine::Collection'
|
belongs_to :collection, class_name: 'Jasmine::Collection'
|
||||||
has_many :chunks, class_name: 'Jasmine::DocumentChunk', foreign_key: :document_id, dependent: :delete_all
|
has_many :chunks, class_name: 'Jasmine::DocumentChunk', dependent: :delete_all
|
||||||
|
|
||||||
enum status: { pending: 0, processing: 1, indexed: 2, failed: 3 }
|
enum status: { pending: 0, processing: 1, indexed: 2, failed: 3 }
|
||||||
enum source_type: { manual: 0, upload: 1, url: 2, faq: 3 }
|
enum source_type: { manual: 0, upload: 1, url: 2, faq: 3 }
|
||||||
@ -46,6 +45,7 @@ module Jasmine
|
|||||||
|
|
||||||
def validate_account_consistency
|
def validate_account_consistency
|
||||||
return if collection.nil?
|
return if collection.nil?
|
||||||
|
|
||||||
errors.add(:base, 'Collection account mismatch') if collection.account_id != account_id
|
errors.add(:base, 'Collection account mismatch') if collection.account_id != account_id
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -53,4 +53,3 @@ module Jasmine
|
|||||||
Jasmine::EmbedDocumentJob.perform_later(id)
|
Jasmine::EmbedDocumentJob.perform_later(id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|||||||
@ -26,8 +26,7 @@
|
|||||||
# fk_rails_... (collection_id => jasmine_collections.id)
|
# fk_rails_... (collection_id => jasmine_collections.id)
|
||||||
# fk_rails_... (inbox_id => inboxes.id)
|
# fk_rails_... (inbox_id => inboxes.id)
|
||||||
#
|
#
|
||||||
module Jasmine
|
class Jasmine::InboxCollection < ApplicationRecord
|
||||||
class InboxCollection < ApplicationRecord
|
|
||||||
self.table_name = 'jasmine_inbox_collections'
|
self.table_name = 'jasmine_inbox_collections'
|
||||||
|
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
@ -53,9 +52,8 @@ module Jasmine
|
|||||||
def validate_visibility_rules
|
def validate_visibility_rules
|
||||||
return if collection.nil? || inbox.nil?
|
return if collection.nil? || inbox.nil?
|
||||||
|
|
||||||
if collection.visibility_private? && collection.owner_inbox_id != inbox_id
|
return unless collection.visibility_private? && collection.owner_inbox_id != inbox_id
|
||||||
|
|
||||||
errors.add(:base, 'Private collections can only be linked to their owner inbox')
|
errors.add(:base, 'Private collections can only be linked to their owner inbox')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|||||||
@ -29,8 +29,7 @@
|
|||||||
# fk_rails_... (account_id => accounts.id)
|
# fk_rails_... (account_id => accounts.id)
|
||||||
# fk_rails_... (inbox_id => inboxes.id)
|
# fk_rails_... (inbox_id => inboxes.id)
|
||||||
#
|
#
|
||||||
module Jasmine
|
class Jasmine::InboxConfig < ApplicationRecord
|
||||||
class InboxConfig < ApplicationRecord
|
|
||||||
self.table_name = 'jasmine_inbox_settings'
|
self.table_name = 'jasmine_inbox_settings'
|
||||||
|
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
@ -44,7 +43,7 @@ module Jasmine
|
|||||||
|
|
||||||
def validate_account_consistency
|
def validate_account_consistency
|
||||||
return if inbox.nil?
|
return if inbox.nil?
|
||||||
|
|
||||||
errors.add(:base, 'Inbox account mismatch') if inbox.account_id != account_id
|
errors.add(:base, 'Inbox account mismatch') if inbox.account_id != account_id
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|||||||
@ -28,8 +28,7 @@
|
|||||||
# fk_rails_... (account_id => accounts.id)
|
# fk_rails_... (account_id => accounts.id)
|
||||||
# fk_rails_... (inbox_id => inboxes.id)
|
# fk_rails_... (inbox_id => inboxes.id)
|
||||||
#
|
#
|
||||||
module Jasmine
|
class Jasmine::ToolConfig < ApplicationRecord
|
||||||
class ToolConfig < ApplicationRecord
|
|
||||||
self.table_name = 'jasmine_tool_configs'
|
self.table_name = 'jasmine_tool_configs'
|
||||||
|
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
@ -68,4 +67,3 @@ module Jasmine
|
|||||||
DEFINITIONS
|
DEFINITIONS
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
module Captain
|
# Base module to fix NameError
|
||||||
module Assistant
|
module Captain::Assistant
|
||||||
# Base module to fix NameError
|
# Base module to fix NameError
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|||||||
@ -4,8 +4,7 @@ require 'json'
|
|||||||
require 'openssl'
|
require 'openssl'
|
||||||
require 'base64'
|
require 'base64'
|
||||||
|
|
||||||
module Captain
|
class Captain::InterService
|
||||||
class InterService
|
|
||||||
# Constants for API URLs
|
# Constants for API URLs
|
||||||
AUTH_URL = 'https://cdpj.partners.bancointer.com.br/oauth/v2/token'.freeze
|
AUTH_URL = 'https://cdpj.partners.bancointer.com.br/oauth/v2/token'.freeze
|
||||||
PIX_URL = 'https://cdpj.partners.bancointer.com.br/pix/v2/cob'.freeze
|
PIX_URL = 'https://cdpj.partners.bancointer.com.br/pix/v2/cob'.freeze
|
||||||
@ -118,4 +117,3 @@ module Captain
|
|||||||
http.request(req)
|
http.request(req)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
module Captain
|
class Captain::Reservations::SyncService
|
||||||
module Reservations
|
PLUG_PLAY_API_BASE = 'https://oxpi.com.br/api/PlugPlay/api/Reserva'.freeze
|
||||||
class SyncService
|
|
||||||
PLUG_PLAY_API_BASE = 'https://oxpi.com.br/api/PlugPlay/api/Reserva'
|
|
||||||
|
|
||||||
def initialize(unit)
|
def initialize(unit)
|
||||||
@unit = unit
|
@unit = unit
|
||||||
@ -171,5 +169,3 @@ module Captain
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
module CrmInsights
|
class CrmInsights::ContactSessionCounter
|
||||||
class ContactSessionCounter
|
|
||||||
WINDOW = 24.hours
|
WINDOW = 24.hours
|
||||||
|
|
||||||
def initialize(conversation)
|
def initialize(conversation)
|
||||||
@ -28,4 +27,3 @@ module CrmInsights
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
module CrmInsights
|
class CrmInsights::GenerateService < Llm::BaseAiService
|
||||||
class GenerateService < Llm::BaseAiService
|
DEFAULT_MODEL = 'gpt-4o-mini'.freeze
|
||||||
DEFAULT_MODEL = 'gpt-4o-mini'
|
|
||||||
|
|
||||||
def initialize(conversation:, insight:, sessions_count:, last_contact_at:, from_message_id: nil, to_message_id: nil)
|
def initialize(conversation:, insight:, sessions_count:, last_contact_at:, from_message_id: nil, to_message_id: nil)
|
||||||
super()
|
super()
|
||||||
@ -169,4 +168,3 @@ module CrmInsights
|
|||||||
"#{@from_message_id} ate #{@to_message_id}"
|
"#{@from_message_id} ate #{@to_message_id}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
module CrmInsights
|
class CrmInsights::UpdateService
|
||||||
class UpdateService
|
|
||||||
def initialize(conversation:, reason: nil)
|
def initialize(conversation:, reason: nil)
|
||||||
@conversation = conversation
|
@conversation = conversation
|
||||||
@reason = reason
|
@reason = reason
|
||||||
@ -356,4 +355,3 @@ module CrmInsights
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
module Jasmine
|
class Jasmine::BrainService
|
||||||
class BrainService
|
|
||||||
# Default intent keywords for hotel/motel business
|
# Default intent keywords for hotel/motel business
|
||||||
DEFAULT_INTENT_KEYWORDS = {
|
DEFAULT_INTENT_KEYWORDS = {
|
||||||
price_question: %w[preço valor quanto custa fica tabela promoção pernoite diária],
|
price_question: %w[preço valor quanto custa fica tabela promoção pernoite diária],
|
||||||
@ -283,4 +282,3 @@ module Jasmine
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
module Jasmine
|
class Jasmine::EmbeddingService
|
||||||
class EmbeddingService
|
|
||||||
CHUNK_SIZE = 1000
|
CHUNK_SIZE = 1000
|
||||||
CHUNK_OVERLAP = 200
|
CHUNK_OVERLAP = 200
|
||||||
EMBEDDING_DIMENSIONS = 1536 # OpenAI text-embedding-3-small dimensions
|
EMBEDDING_DIMENSIONS = 1536 # OpenAI text-embedding-3-small dimensions
|
||||||
@ -91,7 +90,7 @@ module Jasmine
|
|||||||
else
|
else
|
||||||
# Fallback: Generate deterministic hash-based embedding for testing
|
# Fallback: Generate deterministic hash-based embedding for testing
|
||||||
# This won't provide semantic search but allows the system to function
|
# This won't provide semantic search but allows the system to function
|
||||||
Rails.logger.warn "OpenAI not configured, using fallback embedding for Jasmine"
|
Rails.logger.warn 'OpenAI not configured, using fallback embedding for Jasmine'
|
||||||
generate_fallback_embedding(text)
|
generate_fallback_embedding(text)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -125,5 +124,3 @@ module Jasmine
|
|||||||
ENV.fetch('JASMINE_EMBEDDING_MODEL', 'text-embedding-3-small')
|
ENV.fetch('JASMINE_EMBEDDING_MODEL', 'text-embedding-3-small')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Jasmine
|
class Jasmine::MediaAnalyzerService
|
||||||
class MediaAnalyzerService
|
|
||||||
attr_reader :message
|
attr_reader :message
|
||||||
|
|
||||||
def initialize(message:)
|
def initialize(message:)
|
||||||
@ -16,7 +15,7 @@ module Jasmine
|
|||||||
private
|
private
|
||||||
|
|
||||||
def analyze_audio
|
def analyze_audio
|
||||||
message.attachments.where(file_type: :audio).each do |attachment|
|
message.attachments.where(file_type: :audio).find_each do |attachment|
|
||||||
next if attachment.meta&.dig('transcribed_text').present?
|
next if attachment.meta&.dig('transcribed_text').present?
|
||||||
|
|
||||||
Rails.logger.info "[Jasmine::MediaAnalyzer] Transcribing audio for Attachment #{attachment.id}"
|
Rails.logger.info "[Jasmine::MediaAnalyzer] Transcribing audio for Attachment #{attachment.id}"
|
||||||
@ -33,7 +32,7 @@ module Jasmine
|
|||||||
end
|
end
|
||||||
|
|
||||||
def analyze_images
|
def analyze_images
|
||||||
message.attachments.where(file_type: :image).each do |attachment|
|
message.attachments.where(file_type: :image).find_each do |attachment|
|
||||||
next if attachment.meta&.dig('description').present?
|
next if attachment.meta&.dig('description').present?
|
||||||
|
|
||||||
Rails.logger.info "[Jasmine::MediaAnalyzer] Analyzing image for Attachment #{attachment.id}"
|
Rails.logger.info "[Jasmine::MediaAnalyzer] Analyzing image for Attachment #{attachment.id}"
|
||||||
@ -50,4 +49,3 @@ module Jasmine
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
module Jasmine
|
class Jasmine::SemanticSearchService
|
||||||
class SemanticSearchService
|
|
||||||
CANDIDATES_PER_PRIORITY = 50
|
CANDIDATES_PER_PRIORITY = 50
|
||||||
TOP_K_PER_PRIORITY = 10
|
TOP_K_PER_PRIORITY = 10
|
||||||
MAX_CHUNKS_PER_DOC = 2
|
MAX_CHUNKS_PER_DOC = 2
|
||||||
@ -175,4 +174,3 @@ module Jasmine
|
|||||||
vector.to_s
|
vector.to_s
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
require 'rest-client'
|
require 'rest-client'
|
||||||
|
|
||||||
module Jasmine
|
class Jasmine::ToolRunner
|
||||||
class ToolRunner
|
|
||||||
attr_reader :inbox, :tool_key, :config
|
attr_reader :inbox, :tool_key, :config
|
||||||
|
|
||||||
def initialize(inbox, tool_key)
|
def initialize(inbox, tool_key)
|
||||||
@ -13,7 +12,7 @@ module Jasmine
|
|||||||
def run
|
def run
|
||||||
definition = Jasmine::ToolConfig::DEFINITIONS[@tool_key]
|
definition = Jasmine::ToolConfig::DEFINITIONS[@tool_key]
|
||||||
raise "Tool not found definition: #{@tool_key}" unless definition
|
raise "Tool not found definition: #{@tool_key}" unless definition
|
||||||
raise "Tool not configured or disabled" unless config&.is_enabled?
|
raise 'Tool not configured or disabled' unless config&.is_enabled?
|
||||||
|
|
||||||
start_time = Time.current
|
start_time = Time.current
|
||||||
|
|
||||||
@ -21,7 +20,7 @@ module Jasmine
|
|||||||
response = make_request(definition)
|
response = make_request(definition)
|
||||||
duration = (Time.current - start_time) * 1000
|
duration = (Time.current - start_time) * 1000
|
||||||
|
|
||||||
success = (200..299).include?(response.code)
|
success = (200..299).cover?(response.code)
|
||||||
body = response.body.to_s
|
body = response.body.to_s
|
||||||
|
|
||||||
# Save stats
|
# Save stats
|
||||||
@ -38,7 +37,11 @@ module Jasmine
|
|||||||
error_msg = e.message
|
error_msg = e.message
|
||||||
|
|
||||||
# Try to parse body from error response if available
|
# Try to parse body from error response if available
|
||||||
error_body = e.response&.body rescue nil
|
error_body = begin
|
||||||
|
e.response&.body
|
||||||
|
rescue StandardError
|
||||||
|
nil
|
||||||
|
end
|
||||||
error_msg = "#{error_msg} - #{error_body}" if error_body
|
error_msg = "#{error_msg} - #{error_body}" if error_body
|
||||||
|
|
||||||
sanitized_error = sanitize(error_msg)
|
sanitized_error = sanitize(error_msg)
|
||||||
@ -97,8 +100,8 @@ module Jasmine
|
|||||||
end
|
end
|
||||||
|
|
||||||
def sanitize(text)
|
def sanitize(text)
|
||||||
return text unless @config&.plug_play_token.present?
|
return text if @config&.plug_play_token.blank?
|
||||||
|
|
||||||
text.to_s.gsub(@config.plug_play_token, '****')
|
text.to_s.gsub(@config.plug_play_token, '****')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|||||||
@ -3,8 +3,7 @@
|
|||||||
require 'openai'
|
require 'openai'
|
||||||
require 'base64'
|
require 'base64'
|
||||||
|
|
||||||
module Jasmine
|
class Jasmine::VisionService
|
||||||
class VisionService
|
|
||||||
attr_reader :attachment
|
attr_reader :attachment
|
||||||
|
|
||||||
def initialize(attachment:)
|
def initialize(attachment:)
|
||||||
@ -66,4 +65,3 @@ module Jasmine
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
module Llm
|
class Llm::BaseAiService
|
||||||
class BaseAiService
|
|
||||||
def initialize
|
def initialize
|
||||||
# Base initialization logic if needed
|
# Base initialization logic if needed
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@ module Whatsapp::BaileysHandlers::Helpers # rubocop:disable Metrics/ModuleLength
|
|||||||
!@raw_message[:key][:fromMe]
|
!@raw_message[:key][:fromMe]
|
||||||
end
|
end
|
||||||
|
|
||||||
def jid_type # rubocop:disable Metrics/CyclomaticComplexity
|
def jid_type
|
||||||
jid = @raw_message[:key][:remoteJid]
|
jid = @raw_message[:key][:remoteJid]
|
||||||
server = jid.split('@').last
|
server = jid.split('@').last
|
||||||
|
|
||||||
@ -39,7 +39,7 @@ module Whatsapp::BaileysHandlers::Helpers # rubocop:disable Metrics/ModuleLength
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def message_type # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity,Metrics/MethodLength,Metrics/AbcSize
|
def message_type # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
|
||||||
msg = unwrap_ephemeral_message(@raw_message[:message])
|
msg = unwrap_ephemeral_message(@raw_message[:message])
|
||||||
if msg.key?(:conversation) || msg.dig(:extendedTextMessage, :text).present?
|
if msg.key?(:conversation) || msg.dig(:extendedTextMessage, :text).present?
|
||||||
'text'
|
'text'
|
||||||
@ -69,7 +69,7 @@ module Whatsapp::BaileysHandlers::Helpers # rubocop:disable Metrics/ModuleLength
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def message_content # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity,Metrics/MethodLength
|
def message_content # rubocop:disable Metrics/MethodLength
|
||||||
msg = unwrap_ephemeral_message(@raw_message[:message])
|
msg = unwrap_ephemeral_message(@raw_message[:message])
|
||||||
case message_type
|
case message_type
|
||||||
when 'text'
|
when 'text'
|
||||||
@ -96,7 +96,7 @@ module Whatsapp::BaileysHandlers::Helpers # rubocop:disable Metrics/ModuleLength
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def reply_to_message_id # rubocop:disable Metrics/CyclomaticComplexity
|
def reply_to_message_id
|
||||||
msg = unwrap_ephemeral_message(@raw_message[:message])
|
msg = unwrap_ephemeral_message(@raw_message[:message])
|
||||||
message_key = case message_type
|
message_key = case message_type
|
||||||
when 'text' then :extendedTextMessage
|
when 'text' then :extendedTextMessage
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
module Whatsapp::BaileysHandlers::MessagesUpsert # rubocop:disable Metrics/ModuleLength
|
module Whatsapp::BaileysHandlers::MessagesUpsert
|
||||||
include Whatsapp::BaileysHandlers::Helpers
|
include Whatsapp::BaileysHandlers::Helpers
|
||||||
include BaileysHelper
|
include BaileysHelper
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
module Whatsapp
|
class Whatsapp::DecryptionService
|
||||||
class DecryptionService
|
|
||||||
require 'openssl'
|
require 'openssl'
|
||||||
require 'base64'
|
require 'base64'
|
||||||
require 'net/http'
|
require 'net/http'
|
||||||
@ -144,4 +143,3 @@ module Whatsapp
|
|||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|||||||
@ -137,7 +137,7 @@ module Whatsapp::IncomingMessageServiceHelpers
|
|||||||
|
|
||||||
# 2. Media Messages (Image, Video, Audio, Document, Sticker)
|
# 2. Media Messages (Image, Video, Audio, Document, Sticker)
|
||||||
[:imageMessage, :videoMessage, :audioMessage, :documentMessage, :stickerMessage].each do |media_key|
|
[:imageMessage, :videoMessage, :audioMessage, :documentMessage, :stickerMessage].each do |media_key|
|
||||||
next unless message.dig(media_key.to_s, 'contextInfo').present?
|
next if message.dig(media_key.to_s, 'contextInfo').blank?
|
||||||
|
|
||||||
ctx = message[media_key.to_s]['contextInfo']
|
ctx = message[media_key.to_s]['contextInfo']
|
||||||
@in_reply_to_external_id = ctx['stanzaID'] || ctx['stanzaId']
|
@in_reply_to_external_id = ctx['stanzaID'] || ctx['stanzaId']
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
module Whatsapp
|
class Whatsapp::IncomingMessageWuzapiService < IncomingMessageBaseService
|
||||||
class IncomingMessageWuzapiService < IncomingMessageBaseService
|
|
||||||
def perform
|
def perform
|
||||||
parser = Whatsapp::Providers::Wuzapi::PayloadParser.new(params)
|
parser = Whatsapp::Providers::Wuzapi::PayloadParser.new(params)
|
||||||
Rails.logger.info "WuzapiService: Processing #{parser.message_type} from #{parser.sender_phone_number}"
|
Rails.logger.info "WuzapiService: Processing #{parser.message_type} from #{parser.sender_phone_number}"
|
||||||
@ -247,4 +246,3 @@ module Whatsapp
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|||||||
@ -1,7 +1,4 @@
|
|||||||
module Whatsapp
|
class Whatsapp::Providers::Wuzapi::PayloadParser
|
||||||
module Providers
|
|
||||||
module Wuzapi
|
|
||||||
class PayloadParser
|
|
||||||
attr_reader :params
|
attr_reader :params
|
||||||
|
|
||||||
def initialize(params)
|
def initialize(params)
|
||||||
@ -82,7 +79,7 @@ module Whatsapp
|
|||||||
# 2. Media Types direct contextInfo
|
# 2. Media Types direct contextInfo
|
||||||
[:imageMessage, :videoMessage, :audioMessage, :stickerMessage, :documentMessage].each do |key|
|
[:imageMessage, :videoMessage, :audioMessage, :stickerMessage, :documentMessage].each do |key|
|
||||||
ctx = msg.dig(key, :contextInfo)
|
ctx = msg.dig(key, :contextInfo)
|
||||||
next unless ctx.present?
|
next if ctx.blank?
|
||||||
|
|
||||||
Rails.logger.info "WuzAPI Reply Debug: Found #{key} contextInfo = #{ctx.inspect}"
|
Rails.logger.info "WuzAPI Reply Debug: Found #{key} contextInfo = #{ctx.inspect}"
|
||||||
stanza = ctx[:stanzaID] || ctx[:stanzaId]
|
stanza = ctx[:stanzaID] || ctx[:stanzaId]
|
||||||
@ -99,7 +96,7 @@ module Whatsapp
|
|||||||
end
|
end
|
||||||
|
|
||||||
# 4. Check for simple conversation with contextInfo (text reply without extendedTextMessage)
|
# 4. Check for simple conversation with contextInfo (text reply without extendedTextMessage)
|
||||||
if msg[:conversation].present? && msg.dig(:contextInfo).present?
|
if msg[:conversation].present? && msg[:contextInfo].present?
|
||||||
ctx = msg[:contextInfo]
|
ctx = msg[:contextInfo]
|
||||||
Rails.logger.info "WuzAPI Reply Debug: Found conversation contextInfo = #{ctx.inspect}"
|
Rails.logger.info "WuzAPI Reply Debug: Found conversation contextInfo = #{ctx.inspect}"
|
||||||
stanza = ctx[:stanzaID] || ctx[:stanzaId]
|
stanza = ctx[:stanzaID] || ctx[:stanzaId]
|
||||||
@ -170,7 +167,7 @@ module Whatsapp
|
|||||||
return Time.current if timestamp_val.blank?
|
return Time.current if timestamp_val.blank?
|
||||||
|
|
||||||
begin
|
begin
|
||||||
Time.parse(timestamp_val.to_s)
|
Time.zone.parse(timestamp_val.to_s)
|
||||||
rescue ArgumentError
|
rescue ArgumentError
|
||||||
Time.current
|
Time.current
|
||||||
end
|
end
|
||||||
@ -210,6 +207,3 @@ module Whatsapp
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
module Whatsapp::Providers
|
class Whatsapp::Providers::WuzapiService < BaseService
|
||||||
class WuzapiService < BaseService
|
|
||||||
attr_reader :whatsapp_channel
|
attr_reader :whatsapp_channel
|
||||||
|
|
||||||
def initialize(whatsapp_channel:)
|
def initialize(whatsapp_channel:)
|
||||||
@ -170,9 +169,8 @@ module Whatsapp::Providers
|
|||||||
target_message = message.conversation.messages.find_by(source_id: message.in_reply_to_external_id)
|
target_message = message.conversation.messages.find_by(source_id: message.in_reply_to_external_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
return false unless target_message.present?
|
return false if target_message.blank?
|
||||||
|
|
||||||
target_message.outgoing? || target_message.template?
|
target_message.outgoing? || target_message.template?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|||||||
@ -39,7 +39,7 @@ module Whatsapp::ZapiHandlers::ReceivedCallback # rubocop:disable Metrics/Module
|
|||||||
!@raw_message.key?(:notification)
|
!@raw_message.key?(:notification)
|
||||||
end
|
end
|
||||||
|
|
||||||
def message_type # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
def message_type
|
||||||
return 'text' if @raw_message.key?(:text)
|
return 'text' if @raw_message.key?(:text)
|
||||||
return 'reaction' if @raw_message.key?(:reaction)
|
return 'reaction' if @raw_message.key?(:reaction)
|
||||||
return 'audio' if @raw_message.key?(:audio)
|
return 'audio' if @raw_message.key?(:audio)
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
module Wuzapi
|
class Wuzapi::ProvisioningService
|
||||||
class ProvisioningService
|
|
||||||
def initialize(base_url, admin_token)
|
def initialize(base_url, admin_token)
|
||||||
@base_url = base_url
|
@base_url = base_url
|
||||||
@admin_token = admin_token
|
@admin_token = admin_token
|
||||||
@ -21,7 +20,8 @@ module Wuzapi
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def setup_webhook(user_token, inbox_id, _webhook_secret) # [INTENTIONAL] reserved for signed webhooks
|
# [INTENTIONAL] reserved for signed webhooks
|
||||||
|
def setup_webhook(user_token, inbox_id, _webhook_secret)
|
||||||
# Host logic needs to come from GlobalConfig or Rails.application.routes
|
# Host logic needs to come from GlobalConfig or Rails.application.routes
|
||||||
# Ideally passed in or resolved.
|
# Ideally passed in or resolved.
|
||||||
base_host = ENV.fetch('FRONTEND_URL', 'http://localhost:3000')
|
base_host = ENV.fetch('FRONTEND_URL', 'http://localhost:3000')
|
||||||
@ -32,4 +32,3 @@ module Wuzapi
|
|||||||
@client.set_webhook(user_token, webhook_url)
|
@client.set_webhook(user_token, webhook_url)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
Rails.application.configure do
|
Rails.application.configure do
|
||||||
config.active_record.encryption.primary_key = ENV.fetch('ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY', 'test_primary_key_must_be_32_bytes_long!!')
|
config.active_record.encryption.primary_key = ENV.fetch('ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY', 'test_primary_key_must_be_32_bytes_long!!')
|
||||||
config.active_record.encryption.deterministic_key = ENV.fetch('ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY', 'test_deterministic_key_must_be_32_bytes!!')
|
config.active_record.encryption.deterministic_key = ENV.fetch('ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY',
|
||||||
config.active_record.encryption.key_derivation_salt = ENV.fetch('ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT', 'test_salt_must_be_unique_and_random')
|
'test_deterministic_key_must_be_32_bytes!!')
|
||||||
|
config.active_record.encryption.key_derivation_salt = ENV.fetch('ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT',
|
||||||
|
'test_salt_must_be_unique_and_random')
|
||||||
end
|
end
|
||||||
|
|||||||
@ -15,7 +15,7 @@ Rails.application.config.after_initialize do
|
|||||||
Rails.logger.error "[RubyLLM] Init failed: #{e.class} #{e.message}"
|
Rails.logger.error "[RubyLLM] Init failed: #{e.class} #{e.message}"
|
||||||
end
|
end
|
||||||
Rails.logger.info "[RubyLLM] Configured with OPENAI_API_KEY: #{api_key[0..10]}..."
|
Rails.logger.info "[RubyLLM] Configured with OPENAI_API_KEY: #{api_key[0..10]}..."
|
||||||
puts "[RubyLLM] Configured with OPENAI_API_KEY: #{api_key[0..10]}..." # Log to stdout for rails runner visibility
|
Rails.logger.debug { "[RubyLLM] Configured with OPENAI_API_KEY: #{api_key[0..10]}..." } # Log to stdout for rails runner visibility
|
||||||
else
|
else
|
||||||
Rails.logger.warn '[RubyLLM] No OPENAI_API_KEY found in environment'
|
Rails.logger.warn '[RubyLLM] No OPENAI_API_KEY found in environment'
|
||||||
end
|
end
|
||||||
|
|||||||
0
configure_captain_webhook.rb
Normal file → Executable file
0
configure_captain_webhook.rb
Normal file → Executable file
@ -8,4 +8,3 @@ class AddBrainFieldsToJasmineInboxSettings < ActiveRecord::Migration[7.1]
|
|||||||
add_column :jasmine_inbox_settings, :intent_keywords, :jsonb, default: {}
|
add_column :jasmine_inbox_settings, :intent_keywords, :jsonb, default: {}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,7 @@ class FixStatusSuitesHeaders < ActiveRecord::Migration[7.1]
|
|||||||
tools = Captain::CustomTool.where('endpoint_url ILIKE ? OR title ILIKE ?', '%/api/PlugPlay/api/SuitesStatus%', '%Status Suites%')
|
tools = Captain::CustomTool.where('endpoint_url ILIKE ? OR title ILIKE ?', '%/api/PlugPlay/api/SuitesStatus%', '%Status Suites%')
|
||||||
|
|
||||||
tools.each do |tool|
|
tools.each do |tool|
|
||||||
puts "Processing tool: #{tool.title} (ID: #{tool.id})"
|
Rails.logger.debug { "Processing tool: #{tool.title} (ID: #{tool.id})" }
|
||||||
|
|
||||||
updated = false
|
updated = false
|
||||||
new_auth_config = tool.auth_config || {}
|
new_auth_config = tool.auth_config || {}
|
||||||
@ -20,7 +20,7 @@ class FixStatusSuitesHeaders < ActiveRecord::Migration[7.1]
|
|||||||
%w[PLUG-PLAY-ID PLUG-PLAY-TOKEN].each do |header_key|
|
%w[PLUG-PLAY-ID PLUG-PLAY-TOKEN].each do |header_key|
|
||||||
next unless query_params.key?(header_key)
|
next unless query_params.key?(header_key)
|
||||||
|
|
||||||
puts " Found #{header_key} in URL query params. Moving to headers."
|
Rails.logger.debug { " Found #{header_key} in URL query params. Moving to headers." }
|
||||||
new_auth_config['headers'][header_key] = query_params[header_key]
|
new_auth_config['headers'][header_key] = query_params[header_key]
|
||||||
query_params.delete(header_key)
|
query_params.delete(header_key)
|
||||||
updated = true
|
updated = true
|
||||||
@ -36,16 +36,16 @@ class FixStatusSuitesHeaders < ActiveRecord::Migration[7.1]
|
|||||||
if tool.param_schema.is_a?(Array)
|
if tool.param_schema.is_a?(Array)
|
||||||
original_size = tool.param_schema.size
|
original_size = tool.param_schema.size
|
||||||
tool.param_schema.reject! { |p| %w[PLUG-PLAY-ID PLUG-PLAY-TOKEN].include?(p['name']) }
|
tool.param_schema.reject! { |p| %w[PLUG-PLAY-ID PLUG-PLAY-TOKEN].include?(p['name']) }
|
||||||
puts ' Removed params from param_schema.' if tool.param_schema.size < original_size
|
Rails.logger.debug ' Removed params from param_schema.' if tool.param_schema.size < original_size
|
||||||
end
|
end
|
||||||
|
|
||||||
tool.save!
|
tool.save!
|
||||||
puts ' Tool updated successfully.'
|
Rails.logger.debug ' Tool updated successfully.'
|
||||||
else
|
else
|
||||||
puts ' No keys found in URL query params. Manual update might be required for values.'
|
Rails.logger.debug ' No keys found in URL query params. Manual update might be required for values.'
|
||||||
end
|
end
|
||||||
rescue URI::InvalidURIError # [INTENTIONAL] keep for future logging
|
rescue URI::InvalidURIError # [INTENTIONAL] keep for future logging
|
||||||
puts " Skipping invalid URI: #{tool.endpoint_url}"
|
Rails.logger.debug { " Skipping invalid URI: #{tool.endpoint_url}" }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -6,7 +6,7 @@ if account
|
|||||||
features_config = YAML.safe_load(Rails.root.join('config/features.yml').read)
|
features_config = YAML.safe_load(Rails.root.join('config/features.yml').read)
|
||||||
|
|
||||||
# Select features that should be enabled by default
|
# Select features that should be enabled by default
|
||||||
default_features = features_config.select { |f| f['enabled'] == true }.map { |f| f['name'] }
|
default_features = features_config.select { |f| f['enabled'] == true }.pluck('name')
|
||||||
|
|
||||||
puts "Enabling default features: #{default_features.join(', ')}"
|
puts "Enabling default features: #{default_features.join(', ')}"
|
||||||
|
|
||||||
@ -16,5 +16,5 @@ if account
|
|||||||
puts "New feature flags: #{account.feature_flags}"
|
puts "New feature flags: #{account.feature_flags}"
|
||||||
puts "Inbox Management Enabled? #{account.feature_enabled?('inbox_management')}"
|
puts "Inbox Management Enabled? #{account.feature_enabled?('inbox_management')}"
|
||||||
else
|
else
|
||||||
puts "Account not found!"
|
puts 'Account not found!'
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,8 +1,4 @@
|
|||||||
module Api
|
class Api::V1::Accounts::Captain::BrandsController < Api::V1::Accounts::BaseController
|
||||||
module V1
|
|
||||||
module Accounts
|
|
||||||
module Captain
|
|
||||||
class BrandsController < Api::V1::Accounts::BaseController
|
|
||||||
before_action :fetch_brand, only: [:show, :update, :destroy]
|
before_action :fetch_brand, only: [:show, :update, :destroy]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@ -46,7 +42,3 @@ module Api
|
|||||||
params.require(:brand).permit(:name, suite_categories: [], stay_durations: [], suite_images: {}, suite_keywords: {})
|
params.require(:brand).permit(:name, suite_categories: [], stay_durations: [], suite_images: {}, suite_keywords: {})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|||||||
@ -1,8 +1,4 @@
|
|||||||
module Api
|
class Api::V1::Accounts::Captain::ConfigurationsController < Api::V1::Accounts::BaseController
|
||||||
module V1
|
|
||||||
module Accounts
|
|
||||||
module Captain
|
|
||||||
class ConfigurationsController < Api::V1::Accounts::BaseController
|
|
||||||
before_action :fetch_config
|
before_action :fetch_config
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@ -28,7 +24,3 @@ module Api
|
|||||||
params.require(:configuration).permit(:title, :subtitle, :primary_color, :secondary_color, :active, :logo_url, :phone_number)
|
params.require(:configuration).permit(:title, :subtitle, :primary_color, :secondary_color, :active, :logo_url, :phone_number)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|||||||
@ -1,8 +1,4 @@
|
|||||||
module Api
|
class Api::V1::Accounts::Captain::ExtrasController < Api::V1::Accounts::BaseController
|
||||||
module V1
|
|
||||||
module Accounts
|
|
||||||
module Captain
|
|
||||||
class ExtrasController < Api::V1::Accounts::BaseController
|
|
||||||
before_action :fetch_extra, only: [:show, :update, :destroy]
|
before_action :fetch_extra, only: [:show, :update, :destroy]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@ -46,7 +42,3 @@ module Api
|
|||||||
params.require(:extra).permit(:title, :description, :price, :category)
|
params.require(:extra).permit(:title, :description, :price, :category)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|||||||
@ -1,8 +1,4 @@
|
|||||||
module Api
|
class Api::V1::Accounts::Captain::PricingsController < Api::V1::Accounts::BaseController
|
||||||
module V1
|
|
||||||
module Accounts
|
|
||||||
module Captain
|
|
||||||
class PricingsController < Api::V1::Accounts::BaseController
|
|
||||||
before_action :fetch_pricing, only: [:show, :update, :destroy]
|
before_action :fetch_pricing, only: [:show, :update, :destroy]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@ -61,7 +57,3 @@ module Api
|
|||||||
pricing.inboxes = inboxes
|
pricing.inboxes = inboxes
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|||||||
@ -1,8 +1,4 @@
|
|||||||
module Api
|
class Api::V1::Accounts::Captain::UnitsController < Api::V1::Accounts::BaseController
|
||||||
module V1
|
|
||||||
module Accounts
|
|
||||||
module Captain
|
|
||||||
class UnitsController < Api::V1::Accounts::BaseController
|
|
||||||
before_action :current_account
|
before_action :current_account
|
||||||
before_action -> { check_authorization(::Captain::Assistant) }
|
before_action -> { check_authorization(::Captain::Assistant) }
|
||||||
|
|
||||||
@ -48,7 +44,3 @@ module Api
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|||||||
@ -1,9 +1,4 @@
|
|||||||
module Api
|
class Api::V1::Accounts::Inboxes::Captain::ReminderSettingsController < Api::V1::Accounts::BaseController
|
||||||
module V1
|
|
||||||
module Accounts
|
|
||||||
module Inboxes
|
|
||||||
module Captain
|
|
||||||
class ReminderSettingsController < Api::V1::Accounts::BaseController
|
|
||||||
before_action :fetch_inbox
|
before_action :fetch_inbox
|
||||||
before_action :fetch_or_initialize_settings
|
before_action :fetch_or_initialize_settings
|
||||||
|
|
||||||
@ -42,8 +37,3 @@ module Api
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|||||||
@ -1,8 +1,4 @@
|
|||||||
module Public
|
class Public::Api::V1::Captain::BookingAppController < ApplicationController
|
||||||
module Api
|
|
||||||
module V1
|
|
||||||
module Captain
|
|
||||||
class BookingAppController < ActionController::Base
|
|
||||||
layout false
|
layout false
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@ -10,7 +6,3 @@ module Public
|
|||||||
render :index
|
render :index
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|||||||
@ -1,8 +1,4 @@
|
|||||||
module Public
|
class Public::Api::V1::Captain::MasterDataController < ActionController::API
|
||||||
module Api
|
|
||||||
module V1
|
|
||||||
module Captain
|
|
||||||
class MasterDataController < ActionController::API
|
|
||||||
def show
|
def show
|
||||||
# Assuming account_id is passed or derived from domain
|
# Assuming account_id is passed or derived from domain
|
||||||
account = Account.find(params[:account_id])
|
account = Account.find(params[:account_id])
|
||||||
@ -27,7 +23,3 @@ module Public
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|||||||
@ -1,8 +1,4 @@
|
|||||||
module Public
|
class Public::Api::V1::Captain::ReservationsController < ActionController::API
|
||||||
module Api
|
|
||||||
module V1
|
|
||||||
module Captain
|
|
||||||
class ReservationsController < ActionController::API
|
|
||||||
# Use basic rescue_from for generic error handling to capture 'Inbox not found' or similar if they bubble up
|
# Use basic rescue_from for generic error handling to capture 'Inbox not found' or similar if they bubble up
|
||||||
rescue_from StandardError, with: :handle_standard_error
|
rescue_from StandardError, with: :handle_standard_error
|
||||||
|
|
||||||
@ -166,7 +162,3 @@ module Public
|
|||||||
}, status: :ok
|
}, status: :ok
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|||||||
@ -1,8 +1,4 @@
|
|||||||
module Public
|
class Public::Api::V1::Captain::WebhooksController < ActionController::API
|
||||||
module Api
|
|
||||||
module V1
|
|
||||||
module Captain
|
|
||||||
class WebhooksController < ActionController::API
|
|
||||||
def inter_pix
|
def inter_pix
|
||||||
Rails.logger.info "[Captain::InterWebhook] 🔔 Received Webhook: #{params.to_unsafe_h}"
|
Rails.logger.info "[Captain::InterWebhook] 🔔 Received Webhook: #{params.to_unsafe_h}"
|
||||||
|
|
||||||
@ -48,7 +44,3 @@ module Public
|
|||||||
render json: { status: 'received' }, status: :ok
|
render json: { status: 'received' }, status: :ok
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|||||||
@ -98,7 +98,8 @@ module Captain::ChatHelper
|
|||||||
end
|
end
|
||||||
|
|
||||||
def api_key
|
def api_key
|
||||||
raw_key = @assistant&.api_key.presence || @assistant&.config&.[]('openai_api_key').presence || ENV.fetch('OPENAI_API_KEY', nil) || ENV.fetch('GEMINI_API_KEY', nil)
|
raw_key = @assistant&.api_key.presence || @assistant&.config&.[]('openai_api_key').presence || ENV.fetch('OPENAI_API_KEY',
|
||||||
|
nil) || ENV.fetch('GEMINI_API_KEY', nil)
|
||||||
return nil if raw_key.blank?
|
return nil if raw_key.blank?
|
||||||
|
|
||||||
# Sanitize: Remove common accidental suffixes like image names or whitespace
|
# Sanitize: Remove common accidental suffixes like image names or whitespace
|
||||||
|
|||||||
@ -18,7 +18,7 @@ class Captain::Conversation::ResponseBuilderJob < ApplicationJob
|
|||||||
trigger_media_analysis
|
trigger_media_analysis
|
||||||
|
|
||||||
Rails.logger.info "[ResponseBuilderJob] Captain V2 Enabled? #{captain_v2_enabled?}"
|
Rails.logger.info "[ResponseBuilderJob] Captain V2 Enabled? #{captain_v2_enabled?}"
|
||||||
File.open('/tmp/v2_debug.log', 'a') { |f| f.puts "[#{Time.now}] ResponseBuilderJob: V2 Enabled? #{captain_v2_enabled?}" }
|
File.open('/tmp/v2_debug.log', 'a') { |f| f.puts "[#{Time.zone.now}] ResponseBuilderJob: V2 Enabled? #{captain_v2_enabled?}" }
|
||||||
|
|
||||||
if captain_v2_enabled?
|
if captain_v2_enabled?
|
||||||
generate_response_with_v2
|
generate_response_with_v2
|
||||||
@ -209,7 +209,7 @@ class Captain::Conversation::ResponseBuilderJob < ApplicationJob
|
|||||||
elapsed_time = Time.zone.now - @start_time
|
elapsed_time = Time.zone.now - @start_time
|
||||||
remaining_delay = target_delay - elapsed_time
|
remaining_delay = target_delay - elapsed_time
|
||||||
|
|
||||||
sleep(remaining_delay) if remaining_delay > 0
|
sleep(remaining_delay) if remaining_delay.positive?
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_new_incoming_messages
|
def fetch_new_incoming_messages
|
||||||
@ -217,11 +217,11 @@ class Captain::Conversation::ResponseBuilderJob < ApplicationJob
|
|||||||
all_messages = @conversation.messages.order(:created_at)
|
all_messages = @conversation.messages.order(:created_at)
|
||||||
|
|
||||||
# Find the last message sent by the assistant (outgoing)
|
# Find the last message sent by the assistant (outgoing)
|
||||||
last_outgoing_index = all_messages.rindex { |m| m.outgoing? }
|
last_outgoing_index = all_messages.rindex(&:outgoing?)
|
||||||
|
|
||||||
potential_messages = if last_outgoing_index
|
potential_messages = if last_outgoing_index
|
||||||
# Get all messages after the last outgoing one
|
# Get all messages after the last outgoing one
|
||||||
all_messages[(last_outgoing_index + 1)..-1] || []
|
all_messages[(last_outgoing_index + 1)..] || []
|
||||||
else
|
else
|
||||||
# If no outgoing messages, use all messages
|
# If no outgoing messages, use all messages
|
||||||
all_messages
|
all_messages
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
module Captain
|
class Captain::IntentClassificationJob < ApplicationJob
|
||||||
class IntentClassificationJob < ApplicationJob
|
|
||||||
queue_as :default
|
queue_as :default
|
||||||
|
|
||||||
CATEGORIES = %w[
|
CATEGORIES = %w[
|
||||||
@ -84,4 +83,3 @@ module Captain
|
|||||||
content&.strip&.downcase
|
content&.strip&.downcase
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|||||||
@ -108,7 +108,7 @@ class Captain::Assistant < ApplicationRecord
|
|||||||
next unless tool_class
|
next unless tool_class
|
||||||
|
|
||||||
# Avoid duplicates if tool is already added (e.g. hardcoded ones)
|
# Avoid duplicates if tool is already added (e.g. hardcoded ones)
|
||||||
next if tools.any? { |t| t.is_a?(tool_class) }
|
next if tools.any?(tool_class)
|
||||||
|
|
||||||
tools << tool_class.new(self, conversation: conversation, user: user)
|
tools << tool_class.new(self, conversation: conversation, user: user)
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
module Captain
|
class Captain::Configuration < ApplicationRecord
|
||||||
class Configuration < ApplicationRecord
|
|
||||||
self.table_name = 'captain_configurations'
|
self.table_name = 'captain_configurations'
|
||||||
|
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
@ -7,4 +6,3 @@ module Captain
|
|||||||
validates :account_id, presence: true
|
validates :account_id, presence: true
|
||||||
validates :title, presence: true
|
validates :title, presence: true
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
module Captain
|
class Captain::Reminder < ApplicationRecord
|
||||||
class Reminder < ApplicationRecord
|
|
||||||
self.table_name = 'captain_reminders'
|
self.table_name = 'captain_reminders'
|
||||||
|
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
@ -18,4 +17,3 @@ module Captain
|
|||||||
scope :pending, -> { where(status: :pending) }
|
scope :pending, -> { where(status: :pending) }
|
||||||
scope :due, -> { pending.where('scheduled_at <= ?', Time.current) }
|
scope :due, -> { pending.where('scheduled_at <= ?', Time.current) }
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
module Captain
|
class Captain::Reservation < ApplicationRecord
|
||||||
class Reservation < ApplicationRecord
|
|
||||||
self.table_name = 'captain_reservations'
|
self.table_name = 'captain_reservations'
|
||||||
|
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
@ -34,4 +33,3 @@ module Captain
|
|||||||
|
|
||||||
delegate :name, :email, :phone_number, to: :contact, prefix: true
|
delegate :name, :email, :phone_number, to: :contact, prefix: true
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
module Captain
|
class Captain::Suite < ApplicationRecord
|
||||||
class Suite < ApplicationRecord
|
|
||||||
self.table_name = 'captain_suites'
|
self.table_name = 'captain_suites'
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
|
|
||||||
validates :name, presence: true
|
validates :name, presence: true
|
||||||
validates :category, presence: true
|
validates :category, presence: true
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
module Captain
|
class Captain::ToolConfig < ApplicationRecord
|
||||||
class ToolConfig < ApplicationRecord
|
|
||||||
self.table_name = 'captain_tool_configs'
|
self.table_name = 'captain_tool_configs'
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
belongs_to :inbox, optional: true
|
belongs_to :inbox, optional: true
|
||||||
@ -12,4 +11,3 @@ module Captain
|
|||||||
validates :tool_key, uniqueness: { scope: [:account_id, :inbox_id] }, if: -> { inbox_id.present? }
|
validates :tool_key, uniqueness: { scope: [:account_id, :inbox_id] }, if: -> { inbox_id.present? }
|
||||||
validates :tool_key, uniqueness: { scope: [:account_id, :captain_assistant_id] }, if: -> { captain_assistant_id.present? }
|
validates :tool_key, uniqueness: { scope: [:account_id, :captain_assistant_id] }, if: -> { captain_assistant_id.present? }
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|||||||
@ -32,7 +32,7 @@ class Captain::Assistant::AgentRunnerService
|
|||||||
|
|
||||||
# [FEATURE] Short-circuit for thank you/emoji messages to ensure reaction tool usage
|
# [FEATURE] Short-circuit for thank you/emoji messages to ensure reaction tool usage
|
||||||
Rails.logger.info "[Captain V2] Checking for reaction. Message: #{message_to_process.inspect}"
|
Rails.logger.info "[Captain V2] Checking for reaction. Message: #{message_to_process.inspect}"
|
||||||
File.open('/tmp/v2_debug.log', 'a') { |f| f.puts "[#{Time.now}] AgentRunnerService: checking reaction for #{message_to_process.inspect}" }
|
File.open('/tmp/v2_debug.log', 'a') { |f| f.puts "[#{Time.zone.now}] AgentRunnerService: checking reaction for #{message_to_process.inspect}" }
|
||||||
|
|
||||||
reaction_response = check_and_react_to_message(message_to_process)
|
reaction_response = check_and_react_to_message(message_to_process)
|
||||||
return reaction_response if reaction_response
|
return reaction_response if reaction_response
|
||||||
@ -41,7 +41,7 @@ class Captain::Assistant::AgentRunnerService
|
|||||||
runner = Agents::Runner.with_agents(*agents)
|
runner = Agents::Runner.with_agents(*agents)
|
||||||
runner = add_callbacks_to_runner(runner) if @callbacks.any?
|
runner = add_callbacks_to_runner(runner) if @callbacks.any?
|
||||||
|
|
||||||
puts "[DEBUG V2] Running with agents: #{agents.map(&:name).join(', ')}"
|
Rails.logger.debug { "[DEBUG V2] Running with agents: #{agents.map(&:name).join(', ')}" }
|
||||||
|
|
||||||
# Use assistant's API key if present, otherwise fallback to global config
|
# Use assistant's API key if present, otherwise fallback to global config
|
||||||
result = with_assistant_api_key do
|
result = with_assistant_api_key do
|
||||||
@ -89,7 +89,7 @@ class Captain::Assistant::AgentRunnerService
|
|||||||
end
|
end
|
||||||
|
|
||||||
Rails.logger.info "[Captain V2] Reaction Pre-Check: Text='#{text}' Category=#{matched_category}"
|
Rails.logger.info "[Captain V2] Reaction Pre-Check: Text='#{text}' Category=#{matched_category}"
|
||||||
File.open('/tmp/v2_debug.log', 'a') { |f| f.puts "[#{Time.now}] AgentRunnerService: Text='#{text}' Category=#{matched_category}" }
|
File.open('/tmp/v2_debug.log', 'a') { |f| f.puts "[#{Time.zone.now}] AgentRunnerService: Text='#{text}' Category=#{matched_category}" }
|
||||||
|
|
||||||
if matched_category
|
if matched_category
|
||||||
Rails.logger.info "[Captain V2] Detected #{matched_category}. Executing ReactToMessageTool directly."
|
Rails.logger.info "[Captain V2] Detected #{matched_category}. Executing ReactToMessageTool directly."
|
||||||
@ -140,7 +140,7 @@ class Captain::Assistant::AgentRunnerService
|
|||||||
# Remove the last user message from history because it will be passed as the main message to the runner
|
# Remove the last user message from history because it will be passed as the main message to the runner
|
||||||
last_user_index = message_history.rindex { |msg| msg[:role] == 'user' || msg[:role] == :user }
|
last_user_index = message_history.rindex { |msg| msg[:role] == 'user' || msg[:role] == :user }
|
||||||
filtered_history = if last_user_index
|
filtered_history = if last_user_index
|
||||||
message_history[0...last_user_index] + message_history[(last_user_index + 1)..-1]
|
message_history[0...last_user_index] + message_history[(last_user_index + 1)..]
|
||||||
else
|
else
|
||||||
message_history
|
message_history
|
||||||
end
|
end
|
||||||
@ -328,7 +328,7 @@ class Captain::Assistant::AgentRunnerService
|
|||||||
def sanitize_global_api_key
|
def sanitize_global_api_key
|
||||||
# Force sanitization of the global gem config just in case it's dirty
|
# Force sanitization of the global gem config just in case it's dirty
|
||||||
raw_key = InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_API_KEY')&.value.presence || ENV.fetch('OPENAI_API_KEY', nil)
|
raw_key = InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_API_KEY')&.value.presence || ENV.fetch('OPENAI_API_KEY', nil)
|
||||||
return unless raw_key.present?
|
return if raw_key.blank?
|
||||||
|
|
||||||
sanitized_key = raw_key.to_s.gsub(/\.(png|jpg|jpeg|gif|webp|svg|@2x|@3x).*$/i, '').strip
|
sanitized_key = raw_key.to_s.gsub(/\.(png|jpg|jpeg|gif|webp|svg|@2x|@3x).*$/i, '').strip
|
||||||
Agents.configure { |config| config.openai_api_key = sanitized_key }
|
Agents.configure { |config| config.openai_api_key = sanitized_key }
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Captain
|
class Captain::HandoffWebhookService
|
||||||
class HandoffWebhookService
|
|
||||||
def initialize(conversation:, assistant:, handoff_context: {})
|
def initialize(conversation:, assistant:, handoff_context: {})
|
||||||
@conversation = conversation
|
@conversation = conversation
|
||||||
@assistant = assistant
|
@assistant = assistant
|
||||||
@ -85,7 +84,7 @@ module Captain
|
|||||||
end
|
end
|
||||||
|
|
||||||
def should_retry?
|
def should_retry?
|
||||||
(@assistant.handoff_webhook_config['retry_attempts'] || 0) > 0
|
(@assistant.handoff_webhook_config['retry_attempts'] || 0).positive?
|
||||||
end
|
end
|
||||||
|
|
||||||
def retry_if_needed(error)
|
def retry_if_needed(error)
|
||||||
@ -102,4 +101,3 @@ module Captain
|
|||||||
ChatwootExceptionTracker.new(error, account: @conversation.account).capture_exception
|
ChatwootExceptionTracker.new(error, account: @conversation.account).capture_exception
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
require 'faraday'
|
require 'faraday'
|
||||||
|
|
||||||
module Captain
|
class Captain::Inter::AuthService
|
||||||
module Inter
|
|
||||||
class AuthService
|
|
||||||
API_BASE_URL = 'https://cdpj.partners.bancointer.com.br'.freeze
|
API_BASE_URL = 'https://cdpj.partners.bancointer.com.br'.freeze
|
||||||
TOKEN_URL = '/oauth/v2/token'.freeze
|
TOKEN_URL = '/oauth/v2/token'.freeze
|
||||||
|
|
||||||
@ -57,5 +55,3 @@ module Captain
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
module Captain
|
class Captain::Inter::CobService
|
||||||
module Inter
|
|
||||||
class CobService
|
|
||||||
API_BASE_URL = 'https://cdpj.partners.bancointer.com.br'.freeze
|
API_BASE_URL = 'https://cdpj.partners.bancointer.com.br'.freeze
|
||||||
|
|
||||||
def initialize(reservation)
|
def initialize(reservation)
|
||||||
@ -9,7 +7,7 @@ module Captain
|
|||||||
end
|
end
|
||||||
|
|
||||||
def call
|
def call
|
||||||
raise 'Unit not configured for Pix' unless @unit.inter_pix_key.present?
|
raise 'Unit not configured for Pix' if @unit.inter_pix_key.blank?
|
||||||
|
|
||||||
token = AuthService.new(@unit).token
|
token = AuthService.new(@unit).token
|
||||||
|
|
||||||
@ -32,7 +30,7 @@ module Captain
|
|||||||
# [CRITICAL DEBUG] Log the ENTIRE response to see why it's being cut
|
# [CRITICAL DEBUG] Log the ENTIRE response to see why it's being cut
|
||||||
Rails.logger.info "[BANCO INTER] FULL RESPONSE: #{safe_body}"
|
Rails.logger.info "[BANCO INTER] FULL RESPONSE: #{safe_body}"
|
||||||
File.open(Rails.root.join('log/tool_debug.log'), 'a') do |f|
|
File.open(Rails.root.join('log/tool_debug.log'), 'a') do |f|
|
||||||
f.puts "[#{Time.now}] BANCO INTER RAW BODY: #{safe_body}"
|
f.puts "[#{Time.zone.now}] BANCO INTER RAW BODY: #{safe_body}"
|
||||||
end
|
end
|
||||||
|
|
||||||
persist_charge(data)
|
persist_charge(data)
|
||||||
@ -86,5 +84,3 @@ module Captain
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|||||||
@ -43,7 +43,7 @@ class Captain::Llm::AssistantChatService < Llm::BaseAiService
|
|||||||
|
|
||||||
# 3. Handle Tool Strategy
|
# 3. Handle Tool Strategy
|
||||||
if brain_decision.strategy == :execute_tool
|
if brain_decision.strategy == :execute_tool
|
||||||
File.open(Rails.root.join('log/brain_debug.log'), 'a') { |f| f.puts "[#{Time.now}] BRAIN DECIDED: #{brain_decision.tool_key}" }
|
File.open(Rails.root.join('log/brain_debug.log'), 'a') { |f| f.puts "[#{Time.zone.now}] BRAIN DECIDED: #{brain_decision.tool_key}" }
|
||||||
|
|
||||||
inbox = @conversation.inbox
|
inbox = @conversation.inbox
|
||||||
|
|
||||||
@ -57,7 +57,7 @@ class Captain::Llm::AssistantChatService < Llm::BaseAiService
|
|||||||
additional_data: { message: additional_message, tool_input: brain_decision.tool_input }
|
additional_data: { message: additional_message, tool_input: brain_decision.tool_input }
|
||||||
)
|
)
|
||||||
|
|
||||||
File.open(Rails.root.join('log/brain_debug.log'), 'a') { |f| f.puts "[#{Time.now}] RUNNER RESULT: #{runner_result.inspect}" }
|
File.open(Rails.root.join('log/brain_debug.log'), 'a') { |f| f.puts "[#{Time.zone.now}] RUNNER RESULT: #{runner_result.inspect}" }
|
||||||
|
|
||||||
return { 'response' => runner_result[:body][:message] } if runner_result[:fallback] && runner_result.dig(:body, :message).present?
|
return { 'response' => runner_result[:body][:message] } if runner_result[:fallback] && runner_result.dig(:body, :message).present?
|
||||||
|
|
||||||
|
|||||||
@ -59,6 +59,6 @@ class Captain::Llm::ContactIdentityService
|
|||||||
def normalize_name(name)
|
def normalize_name(name)
|
||||||
parts = name.split
|
parts = name.split
|
||||||
parts.shift if parts.first&.downcase.in?(%w[a o])
|
parts.shift if parts.first&.downcase.in?(%w[a o])
|
||||||
parts.map { |part| part.capitalize }.join(' ')
|
parts.map(&:capitalize).join(' ')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
module Captain
|
class Captain::Llm::JasmineBrain
|
||||||
module Llm
|
|
||||||
class JasmineBrain
|
|
||||||
Decision = Struct.new(:strategy, :tool_key, :reasoning, :tool_input, keyword_init: true)
|
Decision = Struct.new(:strategy, :tool_key, :reasoning, :tool_input, keyword_init: true)
|
||||||
|
|
||||||
def self.decide(assistant:, conversation:, message:, history:)
|
def self.decide(assistant:, conversation:, message:, history:)
|
||||||
@ -379,5 +377,3 @@ module Captain
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|||||||
@ -86,7 +86,6 @@ class Captain::Llm::SystemPromptsService
|
|||||||
SYSTEM_PROMPT_MESSAGE
|
SYSTEM_PROMPT_MESSAGE
|
||||||
end
|
end
|
||||||
|
|
||||||
# rubocop:disable Metrics/MethodLength
|
|
||||||
def copilot_response_generator(product_name, available_tools, config = {})
|
def copilot_response_generator(product_name, available_tools, config = {})
|
||||||
citation_guidelines = if config['feature_citation']
|
citation_guidelines = if config['feature_citation']
|
||||||
<<~CITATION_TEXT
|
<<~CITATION_TEXT
|
||||||
@ -149,7 +148,6 @@ class Captain::Llm::SystemPromptsService
|
|||||||
#{available_tools}
|
#{available_tools}
|
||||||
SYSTEM_PROMPT_MESSAGE
|
SYSTEM_PROMPT_MESSAGE
|
||||||
end
|
end
|
||||||
# rubocop:enable Metrics/MethodLength
|
|
||||||
|
|
||||||
def assistant_response_generator(assistant_name, product_name, config = {})
|
def assistant_response_generator(assistant_name, product_name, config = {})
|
||||||
json_instruction = <<~JSON_INSTRUCTION
|
json_instruction = <<~JSON_INSTRUCTION
|
||||||
@ -258,7 +256,7 @@ class Captain::Llm::SystemPromptsService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def assistant_prompt_from_blocks(blocks)
|
def assistant_prompt_from_blocks(blocks)
|
||||||
Array(blocks).map do |block|
|
Array(blocks).filter_map do |block|
|
||||||
title = block['title'] || block[:title]
|
title = block['title'] || block[:title]
|
||||||
content = block['content'] || block[:content]
|
content = block['content'] || block[:content]
|
||||||
next if title.to_s.strip.empty? && content.to_s.strip.empty?
|
next if title.to_s.strip.empty? && content.to_s.strip.empty?
|
||||||
@ -268,7 +266,7 @@ class Captain::Llm::SystemPromptsService
|
|||||||
else
|
else
|
||||||
"[#{title}]\n#{content}".strip
|
"[#{title}]\n#{content}".strip
|
||||||
end
|
end
|
||||||
end.compact.join("\n\n")
|
end.join("\n\n")
|
||||||
end
|
end
|
||||||
|
|
||||||
def paginated_faq_generator(start_page, end_page, language = 'english')
|
def paginated_faq_generator(start_page, end_page, language = 'english')
|
||||||
|
|||||||
@ -19,8 +19,8 @@ class Captain::Reminders::CreateService
|
|||||||
inbox = conversation&.inbox || find_inbox
|
inbox = conversation&.inbox || find_inbox
|
||||||
contact_inbox = conversation&.contact_inbox || find_or_create_contact_inbox(inbox)
|
contact_inbox = conversation&.contact_inbox || find_or_create_contact_inbox(inbox)
|
||||||
|
|
||||||
raise ArgumentError, 'Inbox not found' unless inbox.present?
|
raise ArgumentError, 'Inbox not found' if inbox.blank?
|
||||||
raise ArgumentError, 'Contact not found' unless contact_inbox&.contact.present?
|
raise ArgumentError, 'Contact not found' if contact_inbox&.contact.blank?
|
||||||
|
|
||||||
schedule_time = parse_time(params[:scheduled_at])
|
schedule_time = parse_time(params[:scheduled_at])
|
||||||
raise ArgumentError, 'Scheduled time is required' unless schedule_time
|
raise ArgumentError, 'Scheduled time is required' unless schedule_time
|
||||||
|
|||||||
@ -32,7 +32,7 @@ class Captain::Reminders::Processor
|
|||||||
|
|
||||||
def suite_available?
|
def suite_available?
|
||||||
assistant = inbox.captain_assistant
|
assistant = inbox.captain_assistant
|
||||||
return false unless assistant.present?
|
return false if assistant.blank?
|
||||||
|
|
||||||
tool = Captain::Tools::StatusSuitesTool.new(assistant, conversation: conversation)
|
tool = Captain::Tools::StatusSuitesTool.new(assistant, conversation: conversation)
|
||||||
result = tool.execute
|
result = tool.execute
|
||||||
@ -49,8 +49,8 @@ class Captain::Reminders::Processor
|
|||||||
raise ArgumentError, 'Message content is required' if content.blank?
|
raise ArgumentError, 'Message content is required' if content.blank?
|
||||||
|
|
||||||
target_conversation = conversation || find_or_create_conversation
|
target_conversation = conversation || find_or_create_conversation
|
||||||
raise ArgumentError, 'Conversation not found' unless target_conversation.present?
|
raise ArgumentError, 'Conversation not found' if target_conversation.blank?
|
||||||
raise ArgumentError, 'Assistant not configured for inbox' unless assistant.present?
|
raise ArgumentError, 'Assistant not configured for inbox' if assistant.blank?
|
||||||
|
|
||||||
Current.executed_by = assistant
|
Current.executed_by = assistant
|
||||||
Current.account = @reminder.account
|
Current.account = @reminder.account
|
||||||
|
|||||||
@ -32,8 +32,8 @@ class Captain::Reservations::CreateService
|
|||||||
inbox = conversation&.inbox || find_inbox
|
inbox = conversation&.inbox || find_inbox
|
||||||
contact_inbox = conversation&.contact_inbox || find_or_create_contact_inbox(inbox)
|
contact_inbox = conversation&.contact_inbox || find_or_create_contact_inbox(inbox)
|
||||||
|
|
||||||
raise ArgumentError, 'Inbox not found' unless inbox.present?
|
raise ArgumentError, 'Inbox not found' if inbox.blank?
|
||||||
raise ArgumentError, 'Contact not found' unless contact_inbox&.contact.present?
|
raise ArgumentError, 'Contact not found' if contact_inbox&.contact.blank?
|
||||||
|
|
||||||
check_in_at = parse_time(params[:check_in_at])
|
check_in_at = parse_time(params[:check_in_at])
|
||||||
raise ArgumentError, 'Check-in time is required' unless check_in_at
|
raise ArgumentError, 'Check-in time is required' unless check_in_at
|
||||||
@ -149,13 +149,13 @@ class Captain::Reservations::CreateService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def find_brand(brand_id)
|
def find_brand(brand_id)
|
||||||
return unless brand_id.present?
|
return if brand_id.blank?
|
||||||
|
|
||||||
Captain::Brand.find_by(id: brand_id, account_id: account.id)
|
Captain::Brand.find_by(id: brand_id, account_id: account.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_unit(unit_id)
|
def find_unit(unit_id)
|
||||||
return unless unit_id.present?
|
return if unit_id.blank?
|
||||||
|
|
||||||
Captain::Unit.find_by(id: unit_id, account_id: account.id)
|
Captain::Unit.find_by(id: unit_id, account_id: account.id)
|
||||||
end
|
end
|
||||||
|
|||||||
@ -16,7 +16,7 @@ class Captain::Tools::BaseTool < RubyLLM::Tool
|
|||||||
RubyLLM.logger.debug "Tool #{name} returned: #{result.inspect}"
|
RubyLLM.logger.debug "Tool #{name} returned: #{result.inspect}"
|
||||||
|
|
||||||
return result if result.is_a?(RubyLLM::Tool::Halt)
|
return result if result.is_a?(RubyLLM::Tool::Halt)
|
||||||
return result unless fallback_message.present?
|
return result if fallback_message.blank?
|
||||||
return result unless errorish_result?(result)
|
return result unless errorish_result?(result)
|
||||||
|
|
||||||
fallback_message
|
fallback_message
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
module Captain
|
class Captain::Tools::CheckAvailabilityTool < BaseTool
|
||||||
module Tools
|
|
||||||
class CheckAvailabilityTool < BaseTool
|
|
||||||
def name
|
def name
|
||||||
'check_availability'
|
'check_availability'
|
||||||
end
|
end
|
||||||
@ -127,11 +125,10 @@ module Captain
|
|||||||
if available_options.present?
|
if available_options.present?
|
||||||
msg = "Disponível! Para a suíte #{final_suite_category} em #{target_date&.strftime('%d/%m')}, tenho estas opções: #{available_options}. Pergunte qual duração o cliente prefere."
|
msg = "Disponível! Para a suíte #{final_suite_category} em #{target_date&.strftime('%d/%m')}, tenho estas opções: #{available_options}. Pergunte qual duração o cliente prefere."
|
||||||
Rails.logger.info "[CheckAvailabilityTool] MENU MODE: #{msg}"
|
Rails.logger.info "[CheckAvailabilityTool] MENU MODE: #{msg}"
|
||||||
return msg
|
|
||||||
else
|
else
|
||||||
msg = "Não encontrei tarifas para a suíte #{final_suite_category} nesta data. Confirme o nome da suíte."
|
msg = "Não encontrei tarifas para a suíte #{final_suite_category} nesta data. Confirme o nome da suíte."
|
||||||
return msg
|
|
||||||
end
|
end
|
||||||
|
return msg
|
||||||
end
|
end
|
||||||
|
|
||||||
pricing = pick_pricing_for_duration(pricing_scope, requested_duration)
|
pricing = pick_pricing_for_duration(pricing_scope, requested_duration)
|
||||||
@ -143,7 +140,6 @@ module Captain
|
|||||||
)} (#{pricing.day_range})."
|
)} (#{pricing.day_range})."
|
||||||
persist_last_availability(final_suite_category, requested_duration, pricing, target_date)
|
persist_last_availability(final_suite_category, requested_duration, pricing, target_date)
|
||||||
Rails.logger.info "[CheckAvailabilityTool] SUCCESS: #{msg}"
|
Rails.logger.info "[CheckAvailabilityTool] SUCCESS: #{msg}"
|
||||||
return msg
|
|
||||||
else
|
else
|
||||||
available_options = pricing_scope.map do |p|
|
available_options = pricing_scope.map do |p|
|
||||||
"#{p.duration}: #{ActiveSupport::NumberHelper.number_to_currency(p.price.to_f, unit: 'R$ ', separator: ',', delimiter: '.')}"
|
"#{p.duration}: #{ActiveSupport::NumberHelper.number_to_currency(p.price.to_f, unit: 'R$ ', separator: ',', delimiter: '.')}"
|
||||||
@ -155,9 +151,9 @@ module Captain
|
|||||||
msg = "Não encontrei tarifas cadastradas para a suíte #{final_suite_category} nesta data (#{target_date}). Por favor, confirme se o nome da suíte está correto."
|
msg = "Não encontrei tarifas cadastradas para a suíte #{final_suite_category} nesta data (#{target_date}). Por favor, confirme se o nome da suíte está correto."
|
||||||
end
|
end
|
||||||
|
|
||||||
File.open(Rails.root.join('log/tool_debug.log'), 'a') { |f| f.puts "[#{Time.now}] FAILURE: #{msg}" }
|
File.open(Rails.root.join('log/tool_debug.log'), 'a') { |f| f.puts "[#{Time.zone.now}] FAILURE: #{msg}" }
|
||||||
return msg
|
|
||||||
end
|
end
|
||||||
|
return msg
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.error "[CheckAvailabilityTool] CRITICAL ERROR: #{e.message}\n#{e.backtrace.first(5).join("\n")}"
|
Rails.logger.error "[CheckAvailabilityTool] CRITICAL ERROR: #{e.message}\n#{e.backtrace.first(5).join("\n")}"
|
||||||
raise e
|
raise e
|
||||||
@ -389,7 +385,7 @@ module Captain
|
|||||||
return (start_idx..6).to_a + (0..end_idx).to_a
|
return (start_idx..6).to_a + (0..end_idx).to_a
|
||||||
end
|
end
|
||||||
|
|
||||||
normalized.split(',').map(&:strip).map { |name| mapping[name] }.compact
|
normalized.split(',').map(&:strip).filter_map { |name| mapping[name] }
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse_date_from_text(text)
|
def parse_date_from_text(text)
|
||||||
@ -461,5 +457,3 @@ module Captain
|
|||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
module Captain
|
class Captain::Tools::CreateReservationIntentTool < BaseTool
|
||||||
module Tools
|
|
||||||
class CreateReservationIntentTool < BaseTool
|
|
||||||
def name
|
def name
|
||||||
'create_reservation_intent'
|
'create_reservation_intent'
|
||||||
end
|
end
|
||||||
@ -33,7 +31,7 @@ module Captain
|
|||||||
def execute(*args, **params)
|
def execute(*args, **params)
|
||||||
actual_params = resolve_params(args, params)
|
actual_params = resolve_params(args, params)
|
||||||
File.open(Rails.root.join('log/tool_debug.log'), 'a') do |f|
|
File.open(Rails.root.join('log/tool_debug.log'), 'a') do |f|
|
||||||
f.puts "[#{Time.now}] STARTING CreateReservationIntentTool with params: #{actual_params}"
|
f.puts "[#{Time.zone.now}] STARTING CreateReservationIntentTool with params: #{actual_params}"
|
||||||
end
|
end
|
||||||
|
|
||||||
suite_category = actual_params[:suite]
|
suite_category = actual_params[:suite]
|
||||||
@ -64,7 +62,7 @@ module Captain
|
|||||||
all_incoming = @conversation&.messages&.incoming&.order(created_at: :asc)&.last(10) || []
|
all_incoming = @conversation&.messages&.incoming&.order(created_at: :asc)&.last(10) || []
|
||||||
last_reset_index = all_incoming.rindex { |m| m.content.to_s.downcase.match?(/\b(reiniciar|resetar|comecar de novo)\b/i) }
|
last_reset_index = all_incoming.rindex { |m| m.content.to_s.downcase.match?(/\b(reiniciar|resetar|comecar de novo)\b/i) }
|
||||||
|
|
||||||
relevant_messages = last_reset_index ? all_incoming[(last_reset_index + 1)..-1] : all_incoming
|
relevant_messages = last_reset_index ? all_incoming[(last_reset_index + 1)..] : all_incoming
|
||||||
user_text_post_reset = relevant_messages.map(&:content).join(' ').downcase
|
user_text_post_reset = relevant_messages.map(&:content).join(' ').downcase
|
||||||
user_text_post_reset = ActiveSupport::Inflector.transliterate(user_text_post_reset).gsub(/[^\w\s]/, '')
|
user_text_post_reset = ActiveSupport::Inflector.transliterate(user_text_post_reset).gsub(/[^\w\s]/, '')
|
||||||
|
|
||||||
@ -88,12 +86,12 @@ module Captain
|
|||||||
end
|
end
|
||||||
|
|
||||||
File.open(Rails.root.join('log/tool_debug.log'), 'a') do |f|
|
File.open(Rails.root.join('log/tool_debug.log'), 'a') do |f|
|
||||||
f.puts "[#{Time.now}] INTENT BARRIER SCAN: Looking for #{valid_terms} in '#{user_text_post_reset}' -> Match: #{match_found}"
|
f.puts "[#{Time.zone.now}] INTENT BARRIER SCAN: Looking for #{valid_terms} in '#{user_text_post_reset}' -> Match: #{match_found}"
|
||||||
end
|
end
|
||||||
|
|
||||||
if suite_category.present? && !match_found
|
if suite_category.present? && !match_found
|
||||||
File.open(Rails.root.join('log/tool_debug.log'), 'a') do |f|
|
File.open(Rails.root.join('log/tool_debug.log'), 'a') do |f|
|
||||||
f.puts "[#{Time.now}] INTENT BLOCKED: Suite '#{suite_category}' (or aliases) not found after reset."
|
f.puts "[#{Time.zone.now}] INTENT BLOCKED: Suite '#{suite_category}' (or aliases) not found after reset."
|
||||||
end
|
end
|
||||||
return "Atenção: O usuário ainda não escolheu a suíte '#{suite_category}' nesta nova conversa. Pergunte: 'Qual suíte você gostaria de reservar?'."
|
return "Atenção: O usuário ainda não escolheu a suíte '#{suite_category}' nesta nova conversa. Pergunte: 'Qual suíte você gostaria de reservar?'."
|
||||||
end
|
end
|
||||||
@ -101,40 +99,41 @@ module Captain
|
|||||||
# Global Price Lock: If the price the agent wants to charge
|
# Global Price Lock: If the price the agent wants to charge
|
||||||
# is different from the VERY LAST availability check, block it.
|
# is different from the VERY LAST availability check, block it.
|
||||||
# EXCEPTION: If explicit deposit_value is provided, we trust the agent knows what it's doing (e.g. promo/custom time)
|
# EXCEPTION: If explicit deposit_value is provided, we trust the agent knows what it's doing (e.g. promo/custom time)
|
||||||
if price > 0 && last_availability.present? && !(deposit_override && deposit_override > 0) && price_mismatch?(price, last_availability[:price])
|
if price.positive? && last_availability.present? && !(deposit_override && deposit_override.positive?) && price_mismatch?(price,
|
||||||
|
last_availability[:price])
|
||||||
msg = "ATENÇÃO: Preço (R$ #{format('%.2f',
|
msg = "ATENÇÃO: Preço (R$ #{format('%.2f',
|
||||||
price)}) diverge da última cotação (R$ #{format('%.2f',
|
price)}) diverge da última cotação (R$ #{format('%.2f',
|
||||||
last_availability[:price])} para #{last_availability[:suite]}). NÃO crie a reserva. Corrija o valor ou peça para o usuário confirmar."
|
last_availability[:price])} para #{last_availability[:suite]}). NÃO crie a reserva. Corrija o valor ou peça para o usuário confirmar."
|
||||||
File.open(Rails.root.join('log/tool_debug.log'), 'a') do |f|
|
File.open(Rails.root.join('log/tool_debug.log'), 'a') do |f|
|
||||||
f.puts "[#{Time.now}] PRICE BLOCK: Agent tried #{price} but last quote was #{last_availability[:price]}"
|
f.puts "[#{Time.zone.now}] PRICE BLOCK: Agent tried #{price} but last quote was #{last_availability[:price]}"
|
||||||
end
|
end
|
||||||
return msg
|
return msg
|
||||||
end
|
end
|
||||||
|
|
||||||
if suite_category.blank?
|
if suite_category.blank?
|
||||||
msg = "SYSTEM INFO: Você esqueceu de informar a 'suite'. Pergunte ao cliente qual suíte e duração ele deseja."
|
msg = "SYSTEM INFO: Você esqueceu de informar a 'suite'. Pergunte ao cliente qual suíte e duração ele deseja."
|
||||||
File.open(Rails.root.join('log/tool_debug.log'), 'a') { |f| f.puts "[#{Time.now}] RETURN: #{msg}" }
|
File.open(Rails.root.join('log/tool_debug.log'), 'a') { |f| f.puts "[#{Time.zone.now}] RETURN: #{msg}" }
|
||||||
return msg
|
return msg
|
||||||
end
|
end
|
||||||
|
|
||||||
if price <= 0
|
if price <= 0
|
||||||
msg = 'SYSTEM INFO: Preço inválido. Use consultar_disponibilidade.'
|
msg = 'SYSTEM INFO: Preço inválido. Use consultar_disponibilidade.'
|
||||||
File.open(Rails.root.join('log/tool_debug.log'), 'a') { |f| f.puts "[#{Time.now}] RETURN: #{msg}" }
|
File.open(Rails.root.join('log/tool_debug.log'), 'a') { |f| f.puts "[#{Time.zone.now}] RETURN: #{msg}" }
|
||||||
return msg
|
return msg
|
||||||
end
|
end
|
||||||
|
|
||||||
ensure_conversation_context!
|
ensure_conversation_context!
|
||||||
|
|
||||||
unless @conversation && @conversation.inbox
|
unless @conversation&.inbox
|
||||||
msg = "Erro Crítico: Contexto de conversa não disponível (Conversation/Inbox nil). Params: #{actual_params}"
|
msg = "Erro Crítico: Contexto de conversa não disponível (Conversation/Inbox nil). Params: #{actual_params}"
|
||||||
File.open(Rails.root.join('log/tool_debug.log'), 'a') { |f| f.puts "[#{Time.now}] FAILURE: #{msg}" }
|
File.open(Rails.root.join('log/tool_debug.log'), 'a') { |f| f.puts "[#{Time.zone.now}] FAILURE: #{msg}" }
|
||||||
return msg
|
return msg
|
||||||
end
|
end
|
||||||
|
|
||||||
unit = infer_unit
|
unit = infer_unit
|
||||||
unless unit
|
unless unit
|
||||||
msg = 'Erro: Unidade não encontrada para esta conversa. Verifique se o Inbox está conectado a uma Unidade.'
|
msg = 'Erro: Unidade não encontrada para esta conversa. Verifique se o Inbox está conectado a uma Unidade.'
|
||||||
File.open(Rails.root.join('log/tool_debug.log'), 'a') { |f| f.puts "[#{Time.now}] RETURN: #{msg}" }
|
File.open(Rails.root.join('log/tool_debug.log'), 'a') { |f| f.puts "[#{Time.zone.now}] RETURN: #{msg}" }
|
||||||
return msg
|
return msg
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -149,7 +148,7 @@ module Captain
|
|||||||
.first
|
.first
|
||||||
|
|
||||||
# Determine Final Charge Amount
|
# Determine Final Charge Amount
|
||||||
deposit_amount = if deposit_override && deposit_override > 0
|
deposit_amount = if deposit_override&.positive?
|
||||||
deposit_override
|
deposit_override
|
||||||
else
|
else
|
||||||
price / 2.0
|
price / 2.0
|
||||||
@ -157,7 +156,7 @@ module Captain
|
|||||||
|
|
||||||
if recent_draft && (recent_draft.total_amount.to_f - deposit_amount).abs < 0.1
|
if recent_draft && (recent_draft.total_amount.to_f - deposit_amount).abs < 0.1
|
||||||
msg = "ATENÇÃO: A reserva JÁ FOI CRIADA anteriormente (ID: #{recent_draft.id}). NÃO crie novamente. Apenas CHAME A FERRAMENTA 'generate_pix' AGORA para finalizar."
|
msg = "ATENÇÃO: A reserva JÁ FOI CRIADA anteriormente (ID: #{recent_draft.id}). NÃO crie novamente. Apenas CHAME A FERRAMENTA 'generate_pix' AGORA para finalizar."
|
||||||
File.open(Rails.root.join('log/tool_debug.log'), 'a') { |f| f.puts "[#{Time.now}] IDEMPOTENCY HIT: Reuse draft #{recent_draft.id}" }
|
File.open(Rails.root.join('log/tool_debug.log'), 'a') { |f| f.puts "[#{Time.zone.now}] IDEMPOTENCY HIT: Reuse draft #{recent_draft.id}" }
|
||||||
return msg
|
return msg
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -189,11 +188,11 @@ module Captain
|
|||||||
|
|
||||||
msg = "Reserva iniciada com sucesso! O valor do sinal (50%) é: #{ActiveSupport::NumberHelper.number_to_currency(deposit_amount,
|
msg = "Reserva iniciada com sucesso! O valor do sinal (50%) é: #{ActiveSupport::NumberHelper.number_to_currency(deposit_amount,
|
||||||
unit: 'R$ ', separator: ',', delimiter: '.')}. INSTRUÇÃO: Como a reserva foi criada com sucesso, avise o cliente e CHAME IMEDIATAMENTE a ferramenta 'generate_pix' para entregar o código de pagamento."
|
unit: 'R$ ', separator: ',', delimiter: '.')}. INSTRUÇÃO: Como a reserva foi criada com sucesso, avise o cliente e CHAME IMEDIATAMENTE a ferramenta 'generate_pix' para entregar o código de pagamento."
|
||||||
File.open(Rails.root.join('log/tool_debug.log'), 'a') { |f| f.puts "[#{Time.now}] SUCCESS: #{msg}" }
|
File.open(Rails.root.join('log/tool_debug.log'), 'a') { |f| f.puts "[#{Time.zone.now}] SUCCESS: #{msg}" }
|
||||||
return msg
|
return msg
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
error_msg = "ERRO FATAL NA CRIACAO: #{e.message} | #{e.backtrace.first}"
|
error_msg = "ERRO FATAL NA CRIACAO: #{e.message} | #{e.backtrace.first}"
|
||||||
File.open(Rails.root.join('log/tool_debug.log'), 'a') { |f| f.puts "[#{Time.now}] EXCEPTION: #{error_msg}" }
|
File.open(Rails.root.join('log/tool_debug.log'), 'a') { |f| f.puts "[#{Time.zone.now}] EXCEPTION: #{error_msg}" }
|
||||||
return "Erro técnico ao criar reserva: #{e.message}"
|
return "Erro técnico ao criar reserva: #{e.message}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -307,7 +306,7 @@ module Captain
|
|||||||
end
|
end
|
||||||
|
|
||||||
def infer_from_history
|
def infer_from_history
|
||||||
return {} unless @conversation.present?
|
return {} if @conversation.blank?
|
||||||
|
|
||||||
suite_candidates = available_suite_categories
|
suite_candidates = available_suite_categories
|
||||||
|
|
||||||
@ -360,5 +359,3 @@ module Captain
|
|||||||
match[1].tr('.', '').tr(',', '.').to_f
|
match[1].tr('.', '').tr(',', '.').to_f
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
module Captain
|
class Captain::Tools::Definitions
|
||||||
module Tools
|
|
||||||
class Definitions
|
|
||||||
ALL = {
|
ALL = {
|
||||||
'status_suites' => {
|
'status_suites' => {
|
||||||
type: :http,
|
type: :http,
|
||||||
@ -54,5 +52,3 @@ module Captain
|
|||||||
}
|
}
|
||||||
}.freeze
|
}.freeze
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
module Captain
|
class Captain::Tools::GeneratePixTool < BaseTool
|
||||||
module Tools
|
|
||||||
class GeneratePixTool < BaseTool
|
|
||||||
def name
|
def name
|
||||||
'generate_pix'
|
'generate_pix'
|
||||||
end
|
end
|
||||||
@ -24,7 +22,7 @@ module Captain
|
|||||||
def execute(*args, **params)
|
def execute(*args, **params)
|
||||||
actual_params = resolve_params(args, params)
|
actual_params = resolve_params(args, params)
|
||||||
input_amount = actual_params[:amount].to_s.gsub(/[^\d,.]/, '').tr(',', '.')
|
input_amount = actual_params[:amount].to_s.gsub(/[^\d,.]/, '').tr(',', '.')
|
||||||
override_amount = input_amount.to_f if input_amount.present? && input_amount.to_f > 0
|
override_amount = input_amount.to_f if input_amount.present? && input_amount.to_f.positive?
|
||||||
|
|
||||||
# ... (Validation Logic) ...
|
# ... (Validation Logic) ...
|
||||||
# 1. Validate Contact Info
|
# 1. Validate Contact Info
|
||||||
@ -144,5 +142,3 @@ module Captain
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
module Captain
|
class Captain::Tools::Parsers::StatusSuitesParser
|
||||||
module Tools
|
|
||||||
module Parsers
|
|
||||||
class StatusSuitesParser
|
|
||||||
def self.parse(response_body)
|
def self.parse(response_body)
|
||||||
# Assuming response_body is already a Hash or needs parsing
|
# Assuming response_body is already a Hash or needs parsing
|
||||||
data = response_body.is_a?(String) ? JSON.parse(response_body) : response_body rescue {}
|
data = begin
|
||||||
|
response_body.is_a?(String) ? JSON.parse(response_body) : response_body
|
||||||
|
rescue StandardError
|
||||||
|
{}
|
||||||
|
end
|
||||||
|
|
||||||
# Example normalization logic based on prompt
|
# Example normalization logic based on prompt
|
||||||
{
|
{
|
||||||
@ -24,6 +25,3 @@ module Captain
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
module Captain
|
class Captain::Tools::ReactToMessageTool < BaseTool
|
||||||
module Tools
|
|
||||||
class ReactToMessageTool < BaseTool
|
|
||||||
def name
|
def name
|
||||||
'react_to_message'
|
'react_to_message'
|
||||||
end
|
end
|
||||||
@ -29,7 +27,7 @@ module Captain
|
|||||||
def execute(*args, **params)
|
def execute(*args, **params)
|
||||||
actual_params = resolve_params(args, params)
|
actual_params = resolve_params(args, params)
|
||||||
emoji = actual_params[:emoji]
|
emoji = actual_params[:emoji]
|
||||||
return error_response('Conversation not found') unless @conversation.present?
|
return error_response('Conversation not found') if @conversation.blank?
|
||||||
return error_response('Emoji is required') if emoji.blank?
|
return error_response('Emoji is required') if emoji.blank?
|
||||||
|
|
||||||
# Get the last incoming message from the customer
|
# Get the last incoming message from the customer
|
||||||
@ -76,5 +74,3 @@ module Captain
|
|||||||
{ success: false, error: message }.to_json
|
{ success: false, error: message }.to_json
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
module Captain
|
class Captain::Tools::ReminderTool < BaseTool
|
||||||
module Tools
|
|
||||||
class ReminderTool < BaseTool
|
|
||||||
def self.name
|
def self.name
|
||||||
'reminder'
|
'reminder'
|
||||||
end
|
end
|
||||||
@ -21,7 +19,7 @@ module Captain
|
|||||||
message = actual_params[:message]
|
message = actual_params[:message]
|
||||||
scheduled_at = actual_params[:scheduled_at]
|
scheduled_at = actual_params[:scheduled_at]
|
||||||
minutes_from_now = actual_params[:minutes_from_now]
|
minutes_from_now = actual_params[:minutes_from_now]
|
||||||
return error_response('Conversation not found') unless @conversation.present?
|
return error_response('Conversation not found') if @conversation.blank?
|
||||||
return error_response('Message is required') if message.blank?
|
return error_response('Message is required') if message.blank?
|
||||||
|
|
||||||
schedule_time = parse_schedule_time(scheduled_at, minutes_from_now)
|
schedule_time = parse_schedule_time(scheduled_at, minutes_from_now)
|
||||||
@ -57,5 +55,3 @@ module Captain
|
|||||||
{ success: false, error: message }.to_json
|
{ success: false, error: message }.to_json
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|||||||
@ -45,7 +45,7 @@ class Captain::Tools::SearchDocumentationService < Captain::Tools::BaseTool
|
|||||||
end
|
end
|
||||||
|
|
||||||
def low_confidence?(response)
|
def low_confidence?(response)
|
||||||
return false unless response&.respond_to?(:neighbor_distance)
|
return false unless response.respond_to?(:neighbor_distance)
|
||||||
|
|
||||||
response.neighbor_distance.to_f > LOW_CONFIDENCE_DISTANCE
|
response.neighbor_distance.to_f > LOW_CONFIDENCE_DISTANCE
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
module Captain
|
class Captain::Tools::StatusSuitesTool < BaseTool
|
||||||
module Tools
|
|
||||||
class StatusSuitesTool < BaseTool
|
|
||||||
def self.name
|
def self.name
|
||||||
'status_suites'
|
'status_suites'
|
||||||
end
|
end
|
||||||
@ -57,5 +55,3 @@ module Captain
|
|||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
module Captain
|
class Captain::Tools::SuiteWatchdogTool < BaseTool
|
||||||
module Tools
|
|
||||||
class SuiteWatchdogTool < BaseTool
|
|
||||||
def self.name
|
def self.name
|
||||||
'suite_watchdog'
|
'suite_watchdog'
|
||||||
end
|
end
|
||||||
@ -21,7 +19,7 @@ module Captain
|
|||||||
suite_identifier = actual_params[:suite_identifier]
|
suite_identifier = actual_params[:suite_identifier]
|
||||||
interval_minutes = actual_params[:interval_minutes] || 10
|
interval_minutes = actual_params[:interval_minutes] || 10
|
||||||
message = actual_params[:message]
|
message = actual_params[:message]
|
||||||
return error_response('Conversation not found') unless @conversation.present?
|
return error_response('Conversation not found') if @conversation.blank?
|
||||||
return error_response('Suite identifier is required') if suite_identifier.blank?
|
return error_response('Suite identifier is required') if suite_identifier.blank?
|
||||||
|
|
||||||
reminder = Captain::Reminders::CreateService.new(
|
reminder = Captain::Reminders::CreateService.new(
|
||||||
@ -55,5 +53,3 @@ module Captain
|
|||||||
{ success: false, error: message }.to_json
|
{ success: false, error: message }.to_json
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
require 'net/http'
|
require 'net/http'
|
||||||
require 'uri'
|
require 'uri'
|
||||||
|
|
||||||
module Captain
|
class Captain::Tools::ToolRunner
|
||||||
module Tools
|
|
||||||
class ToolRunner
|
|
||||||
def self.run(assistant:, tool_key:, inbox:, conversation:, additional_data: {})
|
def self.run(assistant:, tool_key:, inbox:, conversation:, additional_data: {})
|
||||||
new(assistant, tool_key, inbox, conversation, additional_data).run
|
new(assistant, tool_key, inbox, conversation, additional_data).run
|
||||||
end
|
end
|
||||||
@ -243,12 +241,10 @@ module Captain
|
|||||||
|
|
||||||
# Specific payloads
|
# Specific payloads
|
||||||
if @tool_key == 'maria_fotos'
|
if @tool_key == 'maria_fotos'
|
||||||
base.merge!(
|
base[:suite_category] = extract_suite_category || 'Indefinida'
|
||||||
suite_category: extract_suite_category || 'Indefinida',
|
base[:message] = 'User requested photos'
|
||||||
message: 'User requested photos'
|
|
||||||
)
|
|
||||||
elsif @tool_key == 'escalar_humano'
|
elsif @tool_key == 'escalar_humano'
|
||||||
base.merge!(reason: 'User requested human agent')
|
base[:reason] = 'User requested human agent'
|
||||||
end
|
end
|
||||||
|
|
||||||
base
|
base
|
||||||
@ -306,11 +302,9 @@ module Captain
|
|||||||
end
|
end
|
||||||
|
|
||||||
def fallback_configured?
|
def fallback_configured?
|
||||||
return false unless @config&.fallback_message.present?
|
return false if @config&.fallback_message.blank?
|
||||||
return false if %w[faq_lookup react_to_message].include?(@tool_key)
|
return false if %w[faq_lookup react_to_message].include?(@tool_key)
|
||||||
|
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
module Captain
|
class Captain::Tools::UpdateContactTool < BaseTool
|
||||||
module Tools
|
|
||||||
class UpdateContactTool < BaseTool
|
|
||||||
def name
|
def name
|
||||||
'update_contact'
|
'update_contact'
|
||||||
end
|
end
|
||||||
@ -29,7 +27,7 @@ module Captain
|
|||||||
def execute(*args, **params)
|
def execute(*args, **params)
|
||||||
actual_params = resolve_params(args, params)
|
actual_params = resolve_params(args, params)
|
||||||
File.open(Rails.root.join('log/tool_debug.log'), 'a') do |f|
|
File.open(Rails.root.join('log/tool_debug.log'), 'a') do |f|
|
||||||
f.puts "[#{Time.now}] STARTING UpdateContactTool with params: #{actual_params}"
|
f.puts "[#{Time.zone.now}] STARTING UpdateContactTool with params: #{actual_params}"
|
||||||
end
|
end
|
||||||
|
|
||||||
name = actual_params[:nome] || actual_params[:name]
|
name = actual_params[:nome] || actual_params[:name]
|
||||||
@ -39,9 +37,9 @@ module Captain
|
|||||||
|
|
||||||
ensure_conversation_context!
|
ensure_conversation_context!
|
||||||
|
|
||||||
unless @conversation && @conversation.contact
|
unless @conversation&.contact
|
||||||
msg = "Erro Crítico: Contexto de conversa ou contato não disponível. Params: #{actual_params}"
|
msg = "Erro Crítico: Contexto de conversa ou contato não disponível. Params: #{actual_params}"
|
||||||
File.open(Rails.root.join('log/tool_debug.log'), 'a') { |f| f.puts "[#{Time.now}] FAILURE: #{msg}" }
|
File.open(Rails.root.join('log/tool_debug.log'), 'a') { |f| f.puts "[#{Time.zone.now}] FAILURE: #{msg}" }
|
||||||
return msg
|
return msg
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -53,13 +51,12 @@ module Captain
|
|||||||
if contact.save
|
if contact.save
|
||||||
update_sticky_state(name: contact.name, cpf: contact.custom_attributes['cpf'])
|
update_sticky_state(name: contact.name, cpf: contact.custom_attributes['cpf'])
|
||||||
msg = "Dados atualizados com sucesso. Nome: #{contact.name}, CPF: #{contact.custom_attributes['cpf']}"
|
msg = "Dados atualizados com sucesso. Nome: #{contact.name}, CPF: #{contact.custom_attributes['cpf']}"
|
||||||
File.open(Rails.root.join('log/tool_debug.log'), 'a') { |f| f.puts "[#{Time.now}] SUCCESS: #{msg}" }
|
File.open(Rails.root.join('log/tool_debug.log'), 'a') { |f| f.puts "[#{Time.zone.now}] SUCCESS: #{msg}" }
|
||||||
msg
|
|
||||||
else
|
else
|
||||||
msg = "Erro ao salvar dados: #{contact.errors.full_messages.join(', ')}"
|
msg = "Erro ao salvar dados: #{contact.errors.full_messages.join(', ')}"
|
||||||
File.open(Rails.root.join('log/tool_debug.log'), 'a') { |f| f.puts "[#{Time.now}] FAILURE: #{msg}" }
|
File.open(Rails.root.join('log/tool_debug.log'), 'a') { |f| f.puts "[#{Time.zone.now}] FAILURE: #{msg}" }
|
||||||
msg
|
|
||||||
end
|
end
|
||||||
|
msg
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@ -89,5 +86,3 @@ module Captain
|
|||||||
Rails.logger.warn "[UpdateContactTool] Failed to update sticky state: #{e.message}"
|
Rails.logger.warn "[UpdateContactTool] Failed to update sticky state: #{e.message}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
module Captain
|
class Captain::WebhookSenderService
|
||||||
class WebhookSenderService
|
|
||||||
def initialize(reservation)
|
def initialize(reservation)
|
||||||
@reservation = reservation
|
@reservation = reservation
|
||||||
@unit = reservation.unit
|
@unit = reservation.unit
|
||||||
end
|
end
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
return unless @unit&.webhook_url.present?
|
return if @unit&.webhook_url.blank?
|
||||||
|
|
||||||
payload = build_payload
|
payload = build_payload
|
||||||
send_webhook(payload)
|
send_webhook(payload)
|
||||||
@ -64,4 +63,3 @@ module Captain
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
module Captain
|
class Captain::WhatsappNotificationService
|
||||||
class WhatsappNotificationService
|
|
||||||
def initialize(reservation)
|
def initialize(reservation)
|
||||||
@reservation = reservation
|
@reservation = reservation
|
||||||
@unit = reservation.unit
|
@unit = reservation.unit
|
||||||
@ -95,4 +94,3 @@ module Captain
|
|||||||
'Estamos ansiosos para recebê-lo!'
|
'Estamos ansiosos para recebê-lo!'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|||||||
@ -47,7 +47,7 @@ module Enterprise::MessageTemplates::HookExecutionService
|
|||||||
def send_typing_indicator
|
def send_typing_indicator
|
||||||
# Access phone number safely via contact association
|
# Access phone number safely via contact association
|
||||||
phone = conversation.contact&.phone_number
|
phone = conversation.contact&.phone_number
|
||||||
return unless phone.present?
|
return if phone.blank?
|
||||||
|
|
||||||
# Assuming Wuzapi is the provider for Channel::Whatsapp in this context
|
# Assuming Wuzapi is the provider for Channel::Whatsapp in this context
|
||||||
# We need to find the Wuzapi client instance or create one.
|
# We need to find the Wuzapi client instance or create one.
|
||||||
|
|||||||
@ -14,7 +14,8 @@ class Llm::BaseAiService
|
|||||||
setup_temperature
|
setup_temperature
|
||||||
end
|
end
|
||||||
|
|
||||||
def chat(model: @model, temperature: @temperature, api_key: nil) # [INTENTIONAL] api_key reserved for per-request auth
|
# [INTENTIONAL] api_key reserved for per-request auth
|
||||||
|
def chat(model: @model, temperature: @temperature, api_key: nil)
|
||||||
client = RubyLLM.chat(model: model)
|
client = RubyLLM.chat(model: model)
|
||||||
# client = client.with_api_key(api_key) if api_key.present?
|
# client = client.with_api_key(api_key) if api_key.present?
|
||||||
client.with_temperature(temperature)
|
client.with_temperature(temperature)
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
class Llm::ModelTestService
|
class Llm::ModelTestService
|
||||||
TEST_PROMPT = 'Reply with the word ok.'
|
TEST_PROMPT = 'Reply with the word ok.'.freeze
|
||||||
|
|
||||||
def initialize(provider:, model:, api_key:)
|
def initialize(provider:, model:, api_key:)
|
||||||
@provider = provider
|
@provider = provider
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user