1+ # frozen_string_literal: true
2+
3+ module DiscourseAi
4+ module InferredConcepts
5+ class Applier
6+ # Associates the provided concepts with a topic
7+ # topic: a Topic instance
8+ # concepts: an array of InferredConcept instances
9+ def self . apply_to_topic ( topic , concepts )
10+ return if topic . blank? || concepts . blank?
11+
12+ concepts . each do |concept |
13+ # Use the join table to associate the concept with the topic
14+ # Avoid duplicates by using find_or_create_by
15+ ActiveRecord ::Base . connection . execute ( <<~SQL )
16+ INSERT INTO topics_inferred_concepts (topic_id, inferred_concept_id, created_at, updated_at)
17+ VALUES (#{ topic . id } , #{ concept . id } , NOW(), NOW())
18+ ON CONFLICT (topic_id, inferred_concept_id) DO NOTHING
19+ SQL
20+ end
21+ end
22+
23+ # Extracts content from a topic for concept analysis
24+ # Returns a string with the topic title and first few posts
25+ def self . topic_content_for_analysis ( topic )
26+ return "" if topic . blank?
27+
28+ # Combine title and first few posts for analysis
29+ posts = Post . where ( topic_id : topic . id ) . order ( :post_number ) . limit ( 10 )
30+
31+ content = "Title: #{ topic . title } \n \n "
32+ content += posts . map do |p |
33+ "#{ p . post_number } ) #{ p . user . username } : #{ p . raw } "
34+ end . join ( "\n \n " )
35+
36+ content
37+ end
38+
39+ # Comprehensive method to analyze a topic and apply concepts
40+ def self . analyze_and_apply ( topic )
41+ return if topic . blank?
42+
43+ # Get content to analyze
44+ content = topic_content_for_analysis ( topic )
45+
46+ # Identify concepts
47+ concept_names = Finder . identify_concepts ( content )
48+
49+ # Create or find concepts in the database
50+ concepts = Finder . create_or_find_concepts ( concept_names )
51+
52+ # Apply concepts to the topic
53+ apply_to_topic ( topic , concepts )
54+
55+ concepts
56+ end
57+
58+ # Match a topic with existing concepts
59+ def self . match_existing_concepts ( topic )
60+ return [ ] if topic . blank?
61+
62+ # Get content to analyze
63+ content = topic_content_for_analysis ( topic )
64+
65+ # Get all existing concepts
66+ existing_concepts = InferredConcept . all . pluck ( :name )
67+ return [ ] if existing_concepts . empty?
68+
69+ # Use the ConceptMatcher persona to match concepts
70+ matched_concept_names = match_concepts_to_content ( content , existing_concepts )
71+
72+ # Find concepts in the database
73+ matched_concepts = InferredConcept . where ( name : matched_concept_names )
74+
75+ # Apply concepts to the topic
76+ apply_to_topic ( topic , matched_concepts )
77+
78+ matched_concepts
79+ end
80+
81+ # Use ConceptMatcher persona to match content against provided concepts
82+ def self . match_concepts_to_content ( content , concept_list )
83+ return [ ] if content . blank? || concept_list . blank?
84+
85+ # Prepare user message with content and concept list
86+ user_message = <<~MESSAGE
87+ Content to analyze:
88+ #{ content }
89+
90+ Available concepts to match:
91+ #{ concept_list . join ( ", " ) }
92+ MESSAGE
93+
94+ # Use the ConceptMatcher persona to match concepts
95+ llm = DiscourseAi ::Completions ::Llm . default_llm
96+ persona = DiscourseAi ::Personas ::ConceptMatcher . new
97+ context = DiscourseAi ::Personas ::BotContext . new (
98+ messages : [ { type : :user , content : user_message } ] ,
99+ user : Discourse . system_user
100+ )
101+
102+ prompt = persona . craft_prompt ( context )
103+ response = llm . completion ( prompt , extract_json : true )
104+
105+ return [ ] unless response . success?
106+
107+ matching_concepts = response . parsed_output [ "matching_concepts" ]
108+ matching_concepts || [ ]
109+ end
110+ end
111+ end
112+ end
0 commit comments