Skip to content

feat: [#916] Implement AI module with provider resolver and OpenAI driver#1421

Merged
hwbrzzl merged 3 commits into
masterfrom
bowen/#916
Mar 28, 2026
Merged

feat: [#916] Implement AI module with provider resolver and OpenAI driver#1421
hwbrzzl merged 3 commits into
masterfrom
bowen/#916

Conversation

@hwbrzzl

@hwbrzzl hwbrzzl commented Mar 25, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Conversation.Prompt no longer accepts context.Context; context is bound once via AI.WithContext(ctx) and reused across all operations
  • A ProviderResolver lazily initialises and caches AI providers from config, with Via accepting a live Provider instance or a func() (Provider, error) factory
  • Built-in OpenAI provider translates AgentPrompt into chat completion requests with a configurable API key, base URL, and default text model (gpt-5.4)

Closes #916

Why

The AI module now has a concrete implementation behind its interfaces. Moving context out of Conversation.Prompt and into AI.WithContext(ctx) keeps the per-prompt call clean: callers bind a context once and every subsequent Prompt and Agent call carries it automatically without repetition.

The new ProviderResolver reads from the ai config key, initialises each driver on first use, and caches it, so the construction cost is paid only once. The Via field accepts either a live Provider for direct injection (e.g. in tests) or a func() (Provider, error) for lazy construction, keeping the bootstrap path flexible.

app := NewApplication(ctx, contractsai.Config{
    Default: "openai",
    Providers: map[string]contractsai.ProviderConfig{
        "openai": {Via: provider},
    },
})

conv, _ := app.Agent(agent, WithModel("gpt-5.4"))
resp, _ := conv.Prompt("What is 2 + 2?")
fmt.Println(resp.Text()) // "Four"

// History is preserved across turns automatically
resp, _ = conv.Prompt("And multiply that by 3?")
fmt.Println(conv.Messages())
// [{user "What is 2 + 2?"} {assistant "Four"} {user "And multiply that by 3?"} ...]

The OpenAI provider maps Agent.Instructions() to the OpenAI system message, Agent.Messages() to the preceding conversation history, and the current Input to the final user message, then calls Chat.Completions.New via the openai-go client. When the provider config omits a default text model, it falls back to gpt-5.4.

image

@hwbrzzl
hwbrzzl requested a review from a team as a code owner March 25, 2026 09:30
@codecov

codecov Bot commented Mar 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.60000% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.19%. Comparing base (c0f099a) to head (fdcf326).
⚠️ Report is 9 commits behind head on master.

Files with missing lines Patch % Lines
ai/provider.go 93.10% 1 Missing and 1 partial ⚠️
ai/conversation.go 95.65% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1421      +/-   ##
==========================================
+ Coverage   65.94%   68.19%   +2.25%     
==========================================
  Files         353      357       +4     
  Lines       26083    27633    +1550     
==========================================
+ Hits        17201    18845    +1644     
+ Misses       8141     7938     -203     
- Partials      741      850     +109     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@hwbrzzl

hwbrzzl commented Mar 25, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Mar 25, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Mar 25, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR introduces a complete AI conversation system with provider abstraction. It implements context-aware application initialization, conversation state management with message history, a pluggable provider resolver, OpenAI provider integration, and comprehensive test coverage. Configuration structures and error definitions support flexible provider configuration.

Changes

Cohort / File(s) Summary
AI Contracts & Configuration
contracts/ai/ai.go, contracts/ai/config.go, contracts/ai/provider.go
Extended AI interface with WithContext() method, removed context parameter from Conversation.Prompt(). Added Config, ProviderConfig, ModelsConfig, and AgentPrompt structures for configuring providers and models. Updated Provider.Prompt() signature to accept AgentPrompt parameter.
Core Application & Conversation
ai/application.go, ai/application_test.go, ai/conversation.go, ai/conversation_test.go
NewApplication now requires context and typed config, stores resolver, initializes providers on-demand. Conversation now manages full message history, appends user/assistant messages on successful prompts, supports reset to initial state. Comprehensive table-driven tests validate all control paths.
Provider Resolution Infrastructure
ai/provider.go, ai/provider_test.go
New ProviderResolver caches and instantiates providers by name, supporting both direct provider instances and factory functions. Tests validate caching behavior and error propagation for missing or misconfigured providers.
OpenAI Provider Implementation
ai/openai/provider.go, ai/openai/provider_test.go, ai/openai/response.go
Complete OpenAI integration: NewOpenAI constructs client with config, Prompt builds chat messages from agent instructions + history + input, calls OpenAI API, returns response with usage tracking. Tests cover successful prompts, model overrides, API errors, empty responses, and request validation via mock HTTP server.
Options & Service Configuration
ai/option.go, ai/option_test.go, ai/service_provider.go, ai/service_provider_test.go
Removed WithTimeout option. Retained WithProvider and WithModel with panic-on-nil-map semantics (table-driven tests). Service provider now unmarshals typed ai config and passes context to NewApplication.
Error & Dependency Definitions
errors/list.go, go.mod
Added AIProviderNotSupported and AIProviderContractNotFulfilled error constants. Added OpenAI and tidwall JSON utilities to indirect dependencies.
Generated Mocks & Documentation
mocks/ai/AI.go, mocks/ai/Conversation.go, mocks/ai/Provider.go, mocks/ai/ProviderCreator.go, .ai/prompts/tests.md, AGENTS.md
Updated AI mock with WithContext(), removed context from Conversation.Prompt(), updated Provider.Prompt() signature, removed obsolete ProviderCreator mock. Added Go testing guidelines document. Renamed "AI Agent Code Rule" to "Code Rules" in contributor guidelines.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant App as Application
    participant Resolver as ProviderResolver
    participant Config as Config
    participant Conv as Conversation
    participant Provider
    participant OpenAI as OpenAI API

    Client->>App: NewApplication(ctx, config)
    App->>Resolver: NewProviderResolver(config)
    
    Client->>App: Agent(agentName, options...)
    App->>App: Extract provider & model from options
    App->>Resolver: New(providerName)
    Resolver->>Config: Lookup provider config
    Config-->>Resolver: ProviderConfig
    Resolver->>Provider: Create provider instance
    Provider-->>Resolver: Provider
    Resolver-->>App: Provider
    App->>Conv: NewConversation(ctx, agent, provider, model)
    Conv->>Conv: Clone agent messages
    Conv-->>App: Conversation
    App-->>Client: Conversation
    
    Client->>Conv: Prompt(input)
    Conv->>Provider: Prompt(ctx, AgentPrompt{agent, input, model})
    Provider->>OpenAI: POST /chat/completions
    OpenAI-->>Provider: Response
    Provider-->>Conv: Response with usage
    Conv->>Conv: Append user message
    Conv->>Conv: Append assistant message
    Conv-->>Client: Response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • feat: [#915] add AI module #1410 — Directly modifies the same AI module files and contracts to implement provider-based conversation system with context handling.

Suggested labels

🚀 Review Ready

Suggested reviewers

  • krishankumar01
  • almas-x

Poem

🐰 A provider resolver hops,
With contexts and configs it shops,
OpenAI chats sing true,
Conversations anew,
Testing keeps logic from flops! ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The linked issue #916 is about updating the carbon/v2 dependency, but the PR implements a complete AI module with provider resolver and OpenAI driver—a fundamentally different feature unrelated to dependency updates. Verify the correct issue number is linked. The PR should reference an issue describing the AI module implementation, not a carbon dependency update.
Docstring Coverage ⚠️ Warning Docstring coverage is 4.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main implementation: AI module with provider resolver and OpenAI driver, matching the substantial code changes across multiple files.
Out of Scope Changes check ✅ Passed All changes are focused on implementing the AI module system (contracts, providers, conversations, OpenAI driver, tests, and service registration). No unrelated modifications were detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bowen/#916

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ai/option.go (1)

3-15: ⚠️ Potential issue | 🟠 Major

Reintroduce WithTimeout to avoid a public API break.

WithTimeout appears removed while contractsai.OptionTimeout still exists. External callers using ai.WithTimeout(...) will fail to compile after this change.

🧩 Compatibility fix
 package ai
 
-import contractsai "github.com/goravel/framework/contracts/ai"
+import (
+	"time"
+
+	contractsai "github.com/goravel/framework/contracts/ai"
+)
@@
 func WithModel(model string) contractsai.Option {
 	return func(options map[string]any) {
 		options[contractsai.OptionModel] = model
 	}
 }
+
+func WithTimeout(timeout time.Duration) contractsai.Option {
+	return func(options map[string]any) {
+		options[contractsai.OptionTimeout] = timeout
+	}
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ai/option.go` around lines 3 - 15, You removed the public helper WithTimeout
while contractsai.OptionTimeout remains, breaking callers; reintroduce a
function WithTimeout(timeout time.Duration) contractsai.Option that sets
options[contractsai.OptionTimeout] = timeout, add the time import if missing,
and place it alongside WithProvider and WithModel so external code using
ai.WithTimeout(...) continues to compile.
🧹 Nitpick comments (1)
ai/conversation_test.go (1)

165-165: Guard against empty slice before indexing.

&tt.initial[0] will panic with index out of bounds if a test case uses an empty initial slice. While current test cases have non-empty slices, this is fragile for future test additions.

Proposed fix
 		assert.Equal(t, tt.expectAfter, resetMessages)
-		assert.NotSame(t, &tt.initial[0], &resetMessages[0])
+		if len(tt.initial) > 0 && len(resetMessages) > 0 {
+			assert.NotSame(t, &tt.initial[0], &resetMessages[0])
+		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ai/conversation_test.go` at line 165, Guard against empty slices before
taking element addresses: before calling assert.NotSame with &tt.initial[0] and
&resetMessages[0], check that both tt.initial and resetMessages have length > 0
(e.g., if len(tt.initial) > 0 && len(resetMessages) > 0 { ... }) and only then
call assert.NotSame; otherwise add an appropriate assertion for empty slices
(e.g., assert.Empty on resetMessages or tt.initial) so the test won’t panic when
initial is empty.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.ai/prompts/tests.md:
- Around line 57-58: The example test uses the wrong loop variable name: it
defines testCases but iterates over tests; update the range loop to iterate over
testCases (or rename the declared slice to tests) so the t.Run block receives
the correct tt values; specifically modify the for _, tt := range ... line to
use testCases to match the declared variable and ensure t.Run(tt.name, func(t
*testing.T) { ... }) uses the matching tt from testCases.

In `@ai/conversation.go`:
- Around line 28-29: The Messages() accessor currently returns the internal
backing slice allowing external mutation; change conversation.Messages() to
return a copy of the slice (e.g. create a new slice and copy/append r.messages
into it) so callers get a cloned slice rather than the original; if Message is a
pointer type also deep-clone elements (or use a per-element clone function) to
prevent callers or Agent implementations from mutating internal state and
bypassing Prompt/Reset—this mirrors the cloning done in NewConversation and
Reset.

In `@ai/openai/provider.go`:
- Around line 46-58: The code unconditionally dereferences prompt.Agent and
silently ignores unknown message roles; update the builder that constructs
messages (the block using prompt.Agent.Instructions(), prompt.Agent.Messages(),
messages, and goopenai.SystemMessage/UserMessage/AssistantMessage) to first
treat a nil prompt.Agent as empty history (skip history creation) instead of
panicking, and when iterating prompt.Agent.Messages() return an explicit error
if a message.Role is not contractsai.RoleUser or contractsai.RoleAssistant (do
not drop it silently); ensure the function returns that error to callers so
unsupported roles fail fast.

In `@ai/option_test.go`:
- Around line 100-104: The test currently uses assert.PanicsWithError but should
use assert.PanicsWithValue; update the assertion in ai/option_test.go so the
call to WithModel inside the loop is wrapped with assert.PanicsWithValue(t,
"assignment to entry in nil map", func() { for _, arg := range tt.args {
WithModel(arg)(nil) } }) instead of assert.PanicsWithError, keeping the same
panic string and the same lambda that invokes WithModel.

In `@ai/provider.go`:
- Around line 23-43: The cache-miss path in ProviderResolver.New currently calls
resolve outside the write lock which allows concurrent New(providerName) calls
to construct the same provider multiple times and potentially cache a nil value;
change the flow to serialize the miss: inside ProviderResolver.New, after the
initial read-lock miss, acquire the write lock (r.mu.Lock), re-check
r.providers[providerName] to avoid TOCTOU, then call r.resolve(providerName,
providerCfg) while holding the lock (or use a per-provider sync.Once keyed by
providerName) and ensure the returned provider is non-nil before assigning it to
r.providers[providerName]; unlock afterwards and return the created provider or
the error.

---

Outside diff comments:
In `@ai/option.go`:
- Around line 3-15: You removed the public helper WithTimeout while
contractsai.OptionTimeout remains, breaking callers; reintroduce a function
WithTimeout(timeout time.Duration) contractsai.Option that sets
options[contractsai.OptionTimeout] = timeout, add the time import if missing,
and place it alongside WithProvider and WithModel so external code using
ai.WithTimeout(...) continues to compile.

---

Nitpick comments:
In `@ai/conversation_test.go`:
- Line 165: Guard against empty slices before taking element addresses: before
calling assert.NotSame with &tt.initial[0] and &resetMessages[0], check that
both tt.initial and resetMessages have length > 0 (e.g., if len(tt.initial) > 0
&& len(resetMessages) > 0 { ... }) and only then call assert.NotSame; otherwise
add an appropriate assertion for empty slices (e.g., assert.Empty on
resetMessages or tt.initial) so the test won’t panic when initial is empty.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 64342517-016b-42d2-a30a-e7669db2b9d7

📥 Commits

Reviewing files that changed from the base of the PR and between de1f301 and fafec5a.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (24)
  • .ai/prompts/tests.md
  • AGENTS.md
  • ai/application.go
  • ai/application_test.go
  • ai/conversation.go
  • ai/conversation_test.go
  • ai/openai/provider.go
  • ai/openai/provider_test.go
  • ai/openai/response.go
  • ai/option.go
  • ai/option_test.go
  • ai/provider.go
  • ai/provider_test.go
  • ai/service_provider.go
  • ai/service_provider_test.go
  • contracts/ai/ai.go
  • contracts/ai/config.go
  • contracts/ai/provider.go
  • errors/list.go
  • go.mod
  • mocks/ai/AI.go
  • mocks/ai/Conversation.go
  • mocks/ai/Provider.go
  • mocks/ai/ProviderCreator.go
💤 Files with no reviewable changes (1)
  • mocks/ai/ProviderCreator.go

Comment thread .ai/prompts/tests.md
Comment thread ai/conversation.go
Comment thread ai/openai/provider.go
Comment thread ai/option_test.go
Comment thread ai/provider.go Outdated

@krishankumar01 krishankumar01 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@hwbrzzl
hwbrzzl merged commit 5753d93 into master Mar 28, 2026
18 checks passed
@hwbrzzl
hwbrzzl deleted the bowen/#916 branch March 28, 2026 01:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants