Class: PartyResearch::Adapters::Gemini
- Defined in:
- app/services/party_research/adapters/gemini.rb
Defined Under Namespace
Classes: GroundedResearch
Constant Summary collapse
- SCHEMA =
JSON Schema for the structured Gemini response. Grounding makes
phone/email/address categories safe to allow — when Gemini
surfaces one of those values, it's coming from a cited public
source (nonprofit directory, business registry, public bio),
not training-data guesswork. { type: 'object', additionalProperties: false, properties: { findings: { type: 'array', description: 'Zero or more findings about this party. Empty when nothing reliable can be inferred.', items: { type: 'object', additionalProperties: false, properties: { category: { type: 'string', enum: %w[name profile note contact_suggestion phone email address social_profile inactive], description: 'Which kind of fact this finding describes.' }, proposed_value: { description: 'String for name/note/phone/email/industry; integer profile_id for ' \ 'profile; object {name,title,email,phone} for contact_suggestion; ' \ 'object {street_line_1,street_line_2,city,state_code,postal_code,country_code} ' \ 'for address (street_line_1 REQUIRED — do not emit an address finding ' \ 'without a full street; city/state/zip alone is not actionable); ' \ 'object {platform,url} for social_profile (platform one of ' \ 'website|facebook|twitter|pinterest|linkedin|instagram|houzz, ' \ 'or any other label — unknown platforms map to a generic website ' \ 'contact point on apply); object {reason} for inactive (short ' \ 'human-readable explanation of why this Party is no longer relevant).' }, confidence: { type: 'integer', minimum: 0, maximum: 100, description: 'How certain you are this finding is correct, 0–100.' }, rationale: { type: 'string', description: 'Source citation (URL or publication name) and short justification.' } }, required: %w[category proposed_value confidence rationale] } } }, required: %w[findings] }.freeze
Constants inherited from Base
Base::ALL, Base::PERSONAL_EMAIL_DOMAINS
Instance Attribute Summary
Attributes inherited from Base
Class Method Summary collapse
-
.arbiter? ⇒ Boolean
Gemini synthesizes findings by evaluating the raw outputs of every other adapter together with Google Search results, so its output is flagged
arbiter: trueand shown above raw adapter findings in the review UI. -
.configured? ⇒ Boolean
Gemini uses the globally-configured API key; treat as configured whenever any key is present in ruby_llm's config.
-
.relevant_for?(_party) ⇒ Boolean
Grounded Gemini is useful for anyone with at least one corroborating signal — homeowner emails resolve to nonprofit directories, business names resolve to licensure records, etc.
Instance Method Summary collapse
Methods inherited from Base
adapter_name, #finding, #initialize, lookup, registered
Constructor Details
This class inherits a constructor from PartyResearch::Adapters::Base
Class Method Details
.arbiter? ⇒ Boolean
Gemini synthesizes findings by evaluating the raw outputs of
every other adapter together with Google Search results, so
its output is flagged arbiter: true and shown above raw
adapter findings in the review UI.
105 106 107 |
# File 'app/services/party_research/adapters/gemini.rb', line 105 def arbiter? true end |
.configured? ⇒ Boolean
Gemini uses the globally-configured API key; treat as configured
whenever any key is present in ruby_llm's config.
88 89 90 |
# File 'app/services/party_research/adapters/gemini.rb', line 88 def configured? RubyLLM.config.gemini_api_key.present? end |
.relevant_for?(_party) ⇒ Boolean
Grounded Gemini is useful for anyone with at least one
corroborating signal — homeowner emails resolve to nonprofit
directories, business names resolve to licensure records, etc.
The enrichable_via_research? gate on the controller already
ensures we have some anchoring signal, so we trust that here.
97 98 99 |
# File 'app/services/party_research/adapters/gemini.rb', line 97 def relevant_for?(_party) true end |
Instance Method Details
#findings_for ⇒ Object
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 |
# File 'app/services/party_research/adapters/gemini.rb', line 110 def findings_for raise ConfigurationError, 'Gemini api_key is not configured (config.gemini_api_key)' unless self.class.configured? # A party with no name has nothing for Gemini to anchor on — skip # rather than send a useless prompt. return [] if party.full_name.blank? # Step 1: grounded web research as FREE TEXT. Grounding citations # routinely corrupt structured JSON, so we don't ask for a schema # on this pass. research = grounded_research # A genuinely empty research write-up means there's nothing to # surface — a legitimate (not failed) empty result. return [] if research.text.blank? # Step 2: structure the research into a schema-conformant findings # array in a SEPARATE call with no grounding tool, where structured # output is reliable. findings = structured_findings(research.text) # We had real grounded research but the structuring pass never # returned a findings array — a non-Hash, or a Hash like {} / # {"error":...} that lacks the array. That's a FAILURE, not "found # nothing": raise so the orchestrator marks the adapter failed # (visible + retryable) instead of silently zeroing the run — the # bug that hid a fully-populated result behind a malformed parse # (prod run #21). unless findings.is_a?(Array) raise AdapterError, 'Gemini returned grounded research but its structured-output ' \ "pass did not return a findings array after #{STRUCTURE_ATTEMPTS} attempts" end # Skip non-hash items (Gemini occasionally yields a stray # primitive when its structured output drifts) and items # missing a category — one malformed entry shouldn't abort the # whole run. normalized = findings.filter_map do |item| next unless item.is_a?(Hash) && item['category'].present? finding( category: item['category'], proposed_value: item['proposed_value'], confidence: item['confidence'], evidence: { rationale: item['rationale'].to_s, source: 'gemini' } ) end # A non-personal email domain is a strong anchor — grounded search # should surface the company/person behind it. Zero findings when # grounding never actually ran (no web-search queries, even after a # retry) is far more likely a grounding miss than a true negative, # so fail loudly to get the run retried rather than recording a # confident "no findings" (the prod run #21 failure mode). if normalized.empty? && !research.grounded? && corporate_email_anchor? raise AdapterError, 'Gemini produced no findings and grounding never ran despite a ' \ 'corporate email anchor on file — likely a grounding miss; flagging for retry' end normalized end |