The ihp-openai package provides streaming functions to access GPT-3 and GPT-4 from OpenAI. It’s designed to work seamlessly with IHP AutoRefresh and IHP DataSync, enabling real-time AI-powered features in your application.
Features
- Streaming Completions: Stream AI responses in real-time to your users
- Automatic Retries: API calls are retried up to 10 times with seamless continuation
- AutoRefresh Integration: Works perfectly with IHP’s AutoRefresh for live updates
- Chat API Support: Built on OpenAI’s modern Chat API with GPT-4 support
- Resilient: Retries continue from already-generated tokens, making failures invisible to users
Installation
Ensure you're on latest IHP
Make sure you’re running on the latest master version of IHP.
Add the dependency
Open your project’s default.nix and add ihp-openai to your haskellDeps:let
ihp = ...;
haskellEnv = import "${ihp}/NixSupport/default.nix" {
ihp = ihp;
haskellDeps = p: with p; [
cabal-install
base
wai
text
hlint
p.ihp
ihp-openai # <-- Add this
];
otherDeps = p: with p; [
# Native dependencies, e.g. imagemagick
];
projectPath = ./.;
};
in
haskellEnv
Rebuild your environment
Stop your development server and run:
Basic Example
Here’s a complete example showing how to integrate OpenAI into a Questions controller:
module Web.Controller.Questions where
import Web.Controller.Prelude
import Web.View.Questions.Index
import Web.View.Questions.New
import Web.View.Questions.Edit
import Web.View.Questions.Show
import qualified IHP.OpenAI as GPT
instance Controller QuestionsController where
action QuestionsAction = autoRefresh do
questions <- query @Question
|> orderByDesc #createdAt
|> fetch
render IndexView { .. }
action NewQuestionAction = do
let question = newRecord
|> set #question "What makes haskell so great?"
render NewView { .. }
action CreateQuestionAction = do
let question = newRecord @Question
question
|> fill @'["question"]
|> validateField #question nonEmpty
|> ifValid \case
Left question -> render NewView { .. }
Right question -> do
question <- question |> createRecord
setSuccessMessage "Question created"
fillAnswer question
redirectTo QuestionsAction
action DeleteQuestionAction { questionId } = do
question <- fetch questionId
deleteRecord question
setSuccessMessage "Question deleted"
redirectTo QuestionsAction
fillAnswer :: (?modelContext :: ModelContext) => Question -> IO (Async ())
fillAnswer question = do
-- Put your OpenAI secret key below:
let secretKey = "sk-XXXXXXXX"
-- This should be done with an IHP job worker instead of async
async do
GPT.streamCompletion secretKey
(buildCompletionRequest question)
(clearAnswer question)
(appendToken question)
pure ()
buildCompletionRequest :: Question -> GPT.CompletionRequest
buildCompletionRequest Question { question } =
-- Here you can adjust the parameters of the request
GPT.newCompletionRequest
{ GPT.maxTokens = 512
, GPT.prompt = [trimming|
Question: ${question}
Answer:
|] }
-- | Sets the answer field back to an empty string
clearAnswer :: (?modelContext :: ModelContext) => Question -> IO ()
clearAnswer question = do
sqlExec "UPDATE questions SET answer = '' WHERE id = ?" (Only question.id)
pure ()
-- | Stores a couple of newly received characters to the database
appendToken :: (?modelContext :: ModelContext) => Question -> Text -> IO ()
appendToken question token = do
sqlExec "UPDATE questions SET answer = answer || ? WHERE id = ?" (token, question.id)
pure ()
How It Works
Streaming Architecture
The streamCompletion function takes three callbacks:
- Clear Callback: Called before streaming starts to reset the answer field
- Append Callback: Called with each token as it’s generated
- Completion Request: Configuration for the OpenAI API call
GPT.streamCompletion
secretKey
completionRequest
clearCallback
appendCallback
Database Schema
For the example above, you’ll need a schema like:
CREATE TABLE questions (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
question TEXT NOT NULL,
answer TEXT DEFAULT '',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
);
AutoRefresh Integration
Using autoRefresh in your action enables real-time updates:
action QuestionsAction = autoRefresh do
questions <- query @Question
|> orderByDesc #createdAt
|> fetch
render IndexView { .. }
As tokens are appended to the database, the view automatically refreshes to show the streaming response.
Configuration Options
CompletionRequest
Customize the AI behavior:
GPT.newCompletionRequest
{ GPT.maxTokens = 1024 -- Maximum response length
, GPT.prompt = "Your prompt" -- The input prompt
, GPT.temperature = 0.7 -- Randomness (0-1)
, GPT.model = "gpt-4" -- Model selection
}
Available Models
gpt-4 - Most capable, slower
gpt-4-turbo - Fast and capable
gpt-3.5-turbo - Fast and cost-effective
Error Handling & Retries
The package automatically handles errors and retries:
- Up to 10 retries: Failed API calls are automatically retried
- Continuation: Retries continue from the last successfully generated token
- Transparent: Users never see that a retry occurred
The automatic retry mechanism ensures reliable AI features even with network instability or API rate limits.
Best Practices
1. Use Background Jobs Instead of Async
For production applications, use IHP’s job queue instead of async:
import Web.Job.FillAnswer
action CreateQuestionAction = do
-- ...
question <- question |> createRecord
-- Schedule a background job
newRecord @FillAnswerJob
|> set #questionId question.id
|> create
redirectTo QuestionsAction
2. Store API Keys Securely
Never hardcode API keys. Use environment variables:
import System.Environment (getEnv)
fillAnswer question = do
secretKey <- getEnv "OPENAI_SECRET_KEY"
async do
GPT.streamCompletion secretKey ...
In your start script:
export OPENAI_SECRET_KEY="sk-..."
RunDevServer
3. Add User Feedback
Show loading states while AI is generating:
[hsx|
{forEach questions \question ->
<div class="question">
<h3>{question.question}</h3>
{if isEmpty question.answer
then <div class="spinner">Generating answer...</div>
else <p>{question.answer}</p>
}
</div>
}
|]
4. Implement Rate Limiting
Protect your API quota:
action CreateQuestionAction = do
-- Check user's question count
questionCount <- query @Question
|> filterWhere (#userId, currentUserId)
|> filterWhere (#createdAt, greaterThan (currentTime - 1 hour))
|> fetchCount
when (questionCount >= 5) do
setErrorMessage "Rate limit exceeded. Try again later."
redirectTo QuestionsAction
-- ... continue with question creation
Advanced Usage
Custom Prompts
Create sophisticated prompts:
buildCompletionRequest Question { question, context } =
GPT.newCompletionRequest
{ GPT.maxTokens = 1024
, GPT.prompt = [trimming|
You are a helpful assistant specialized in Haskell.
Context: ${context}
Question: ${question}
Provide a detailed answer with code examples:
|]
, GPT.temperature = 0.7
}
Streaming to Multiple Fields
Stream different parts to different fields:
appendToSummary :: Question -> Text -> IO ()
appendToSummary question token = do
sqlExec "UPDATE questions SET summary = summary || ? WHERE id = ?"
(token, question.id)
appendToDetails :: Question -> Text -> IO ()
appendToDetails question token = do
sqlExec "UPDATE questions SET details = details || ? WHERE id = ?"
(token, question.id)
Troubleshooting
API Key Errors
If you get authentication errors:
- Verify your API key is correct
- Check that the key has proper permissions
- Ensure you haven’t exceeded your quota
Slow Streaming
If responses are slow:
- Check your internet connection
- Try
gpt-3.5-turbo for faster responses
- Reduce
maxTokens to limit response length
AutoRefresh Not Working
Ensure:
- You’re using
autoRefresh in your action
- JavaScript is enabled in the browser
- The WebSocket connection is established
See Also