Skip to content

Commit

Permalink
Generate answers with AI
Browse files Browse the repository at this point in the history
  • Loading branch information
IanPhilips committed Nov 26, 2024
1 parent f51e657 commit 825deef
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 14 deletions.
88 changes: 88 additions & 0 deletions backend/api/src/generate-ai-answers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { APIError, APIHandler } from './helpers/endpoint'
import { track } from 'shared/analytics'
import { largePerplexityModel, perplexity } from 'shared/helpers/perplexity'
import { models, promptClaude } from 'shared/helpers/claude'
import {
addAnswersModeDescription,
multiChoiceOutcomeTypeDescriptions,
} from 'common/ai-creation-prompts'
import { log } from 'shared/utils'

export const generateAIAnswers: APIHandler<'generate-ai-answers'> = async (
props,
auth
) => {
const { question, shouldAnswersSumToOne, description, answers } = props
const answersString = answers.filter(Boolean).join(', ')
const prompt = `Question: ${question} ${
description && description !== '<p></p>'
? `\nDescription: ${description}`
: ''
} ${
answersString.length
? `\nHere are my suggested answers: ${answersString}`
: ''
}`
log('generateAIAnswers prompt', prompt)
const outcomeKey = shouldAnswersSumToOne
? 'DEPENDENT_MULTIPLE_CHOICE'
: 'INDEPENDENT_MULTIPLE_CHOICE'
log('generateAIAnswers', { props })
try {
// First use perplexity to research the topic
const { messages, citations } = await perplexity(prompt, {
model: largePerplexityModel,
systemPrompts: [
`You are a helpful AI assistant that researches information to help generate possible answers for a multiple choice question.`,
],
})

const perplexityResponse =
[messages].join('\n') + '\n\nSources:\n' + citations.join('\n\n')

// Then use Claude to generate the answers
const systemPrompt = `
You are a helpful AI assistant that generates possible answers for multiple choice prediction market questions.
The question type is ${outcomeKey}.
${multiChoiceOutcomeTypeDescriptions}
Guidelines:
- Generate 2-20 possible answers based on the research${
answersString.length
? ` and the user's suggested answers: ${answersString}`
: ''
}, as well as a recommended addAnswersMode
- Answers should be concise and clear
- The addAnswersMode should be one of the following:
${addAnswersModeDescription}
${
answersString.length
? `- Do NOT repeat any of the user's suggested answers, but DO match the style, idea, and range (if numeric) of the user's suggested answers, e.g. return answers of type 11-20, 21-30, etc. if the user suggests 1-10, or use 3-letter months if they suggest Feb, Mar, Apr, etc.`
: ''
}
- ONLY return a single JSON object with "answers" string array and "addAnswersMode" string. Do not return anything else.
Here is current information from the internet that is related to the question:
${perplexityResponse}
`

const claudePrompt = `${prompt}\n\nReturn ONLY a JSON object containing "answers" string array and "addAnswersMode" string.`

const claudeResponse = await promptClaude(claudePrompt, {
model: models.sonnet,
system: systemPrompt,
})
log('claudeResponse', claudeResponse)

const result = JSON.parse(claudeResponse)

track(auth.uid, 'generate-ai-answers', {
question,
})

return result
} catch (e) {
console.error('Failed to generate answers:', e)
throw new APIError(500, 'Failed to generate answers. Please try again.')
}
}
34 changes: 29 additions & 5 deletions backend/api/src/generate-ai-description.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,22 @@ import { track } from 'shared/analytics'
import { anythingToRichText } from 'shared/tiptap'
import { largePerplexityModel, perplexity } from 'shared/helpers/perplexity'
import { models, promptClaude } from 'shared/helpers/claude'
import { outcomeTypeDescriptions } from 'common/ai-creation-prompts'
import {
addAnswersModeDescription,
outcomeTypeDescriptions,
} from 'common/ai-creation-prompts'

export const generateAIDescription: APIHandler<
'generate-ai-description'
> = async (props, auth) => {
const { question, description, answers, outcomeType, shouldAnswersSumToOne } =
props
const {
question,
description,
answers,
outcomeType,
shouldAnswersSumToOne,
addAnswersMode,
} = props
const includeAnswers =
answers &&
answers.length > 0 &&
Expand Down Expand Up @@ -51,16 +60,31 @@ export const generateAIDescription: APIHandler<
? `Their market is of type ${outcomeKey}\n${outcomeTypeDescriptions}`
: ''
}
${
addAnswersMode
? `\nThe user has specified that the addAnswersMode is ${addAnswersMode}\n${addAnswersModeDescription}`
: ''
}
Guidelines:
- Keep descriptions concise but informative
- Incorporate any relevant information from the user's description into your own description
- If the user supplied answers, provide any relevant background information for each answer
- If the market is personal, (i.e. I will attend the most parties, or I will get a girlfriend) word resolution criteria in the first person
- Include relevant sources and data when available
- Clearly state how the market will be resolved
- Try to think of any edge cases or special scenarios that traders should be aware of, mention them in the description and how the market will be resolved in those cases
- Try to think of likely edge cases that traders should be aware of, mention them in the description and how the market will be resolved in those cases
- Don't repeat the question in the description
- Focus on objective facts rather than opinions
- If the market has a precondition, such as 'If I attend, will I enjoy the party?', or 'If Biden runs, will he win?', markets should resolve N/A if the precondition is not met
- Format the response as markdown with sections such as "Background", "Resolution criteria", "Things to consider", etc.
- Format the response as markdown
- Include a "Background" section that includes information readers/traders may want to know if it's relevant to the user's question AND it's not common knowledge. Keep it concise.
- Include a "Resolution criteria" section that describes how the market will be resolved. Include any special resolution criteria for likely edge cases.
- Only include a "Considerations" section if there are unexpected considerations that traders may want to know about. E.g. if the question is about something that has never happened before, etc. ${
addAnswersMode === 'DISABLED' &&
outcomeKey === 'DEPENDENT_MULTIPLE_CHOICE'
? 'E.g. if the answers are not exhaustive, traders should be warned that the market may resolve N/A.'
: ''
}
- Here is current information from the internet that is related to the user's prompt. Include information from it in the description that traders or other readers may want to know if it's relevant to the user's question:
${perplexityResponse}
`
Expand Down
2 changes: 2 additions & 0 deletions backend/api/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ import { getTopicDashboards } from './get-topic-dashboards'
import { generateAIMarketSuggestions } from './generate-ai-market-suggestions'
import { generateAIMarketSuggestions2 } from './generate-ai-market-suggestions-2'
import { generateAIDescription } from './generate-ai-description'
import { generateAIAnswers } from './generate-ai-answers'

// we define the handlers in this object in order to typecheck that every API has a handler
export const handlers: { [k in APIPath]: APIHandler<k> } = {
Expand Down Expand Up @@ -285,4 +286,5 @@ export const handlers: { [k in APIPath]: APIHandler<k> } = {
'generate-ai-market-suggestions': generateAIMarketSuggestions,
'generate-ai-market-suggestions-2': generateAIMarketSuggestions2,
'generate-ai-description': generateAIDescription,
'generate-ai-answers': generateAIAnswers,
}
24 changes: 16 additions & 8 deletions common/src/ai-creation-prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,23 @@ Following each market suggestion, add a "Reasoning:" section that addresses the
1. A clear explanation of why this market follows from the user's prompt and related source material
2. Why it's a good prediction market (e.g., has clear resolution criteria, neither a yes nor no outcome is overwhelmingly likely, etc. from above)
`

export const multiChoiceOutcomeTypeDescriptions = `
- "INDEPENDENT_MULTIPLE_CHOICE" means there are multiple answers, and ANY of them can resolve yes, no, or N/A e.g. What will happen during the next presidential debate? Which companies will express interest in buying twitter?
- "DEPENDENT_MULTIPLE_CHOICE" means there are multiple answers, but ONLY one can resolve yes, (while the rest resolve no, or alternatively the entire market resolves N/A if a precondition is not met) e.g. Who will win the presidential election?, Who will be the first to express interest in buying twitter?
`

export const outcomeTypeDescriptions = `
- "BINARY" means there are only two answers, true (yes) or false (no)
- "INDEPENDENT_MULTIPLE_CHOICE" means there are multiple answers, and ANY of them can resolve yes, no, or N/A e.g. What will happen during the next presidential debate? Which companies will express interest in buying twitter?
- "DEPENDENT_MULTIPLE_CHOICE" means there are multiple answers, but ONLY one can resolve yes, (while the rest resolve no, or alternatively the entire market resolves N/A if a precondition is not met) e.g. Who will win the presidential election?, Who will be the first to express interest in buying twitter?
- "POLL" means the question is about a personal matter, i.e. "Should I move to a new city?", "Should I get a new job?", etc.
- "BINARY" means there are only two answers, true (yes) or false (no)
${multiChoiceOutcomeTypeDescriptions}
- "POLL" means the question is about a personal matter, i.e. "Should I move to a new city?", "Should I get a new job?", etc.
`
export const addAnswersModeDescription = `
- "DISABLED" means that the answers list covers all possible outcomes and no more answers can be added after the market is created
- "ONLY_CREATOR" means that only the creator can add answers after the market is created
- "ANYONE" means that anyone can add answers after the market is created
- If the addAnswersMode is "ONLY_CREATOR" or "ANYONE", while the outcomeType is "DEPENDENT_MULTIPLE_CHOICE", then Manifold will automatically add the 'Other' option to the answers list, so you do not need to include it in the array.
`

export const formattingPrompt = `
Convert these prediction market ideas into valid JSON objects that abide by the following Manifold Market schema. Each object should include:
Expand All @@ -80,10 +91,7 @@ export const formattingPrompt = `
${outcomeTypeDescriptions}
- answers (array of strings, recommended only if outcomeType is one of the "DEPENDENT_MULTIPLE_CHOICE" or "INDEPENDENT_MULTIPLE_CHOICE" types)
- addAnswersMode ("DISABLED", "ONLY_CREATOR", or "ANYONE", required if one of the "DEPENDENT_MULTIPLE_CHOICE" or "INDEPENDENT_MULTIPLE_CHOICE" types is provided)
- "DISABLED" means that the answers list covers all possible outcomes and no more answers can be added after the market is created
- "ONLY_CREATOR" means that only the creator can add answers after the market is created
- "ANYONE" means that anyone can add answers after the market is created
- If the addAnswersMode is "ONLY_CREATOR" or "ANYONE", while the outcomeType is "DEPENDENT_MULTIPLE_CHOICE", then Manifold will automatically add the 'Other' option to the answers list, so you do not need to include it in the array.
${addAnswersModeDescription}
- reasoning (string, required - extract the reasoning section from each market suggestion)`

export const perplexitySystemPrompt = `You are a helpful assistant that creates engaging prediction markets on Manifold Markets.
Expand Down
20 changes: 20 additions & 0 deletions common/src/api/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1841,6 +1841,26 @@ export const API = (_apiTypeCheck = {
answers: z.array(z.string()).optional(),
outcomeType: z.string().optional(),
shouldAnswersSumToOne: coerceBoolean.optional(),
addAnswersMode: z
.enum(['DISABLED', 'ONLY_CREATOR', 'ANYONE'])
.optional(),
})
.strict(),
},
'generate-ai-answers': {
method: 'POST',
visibility: 'public',
authed: true,
returns: {} as {
answers: string[]
addAnswersMode: 'DISABLED' | 'ONLY_CREATOR' | 'ANYONE'
},
props: z
.object({
question: z.string(),
answers: z.array(z.string()),
shouldAnswersSumToOne: coerceBoolean,
description: z.string().optional(),
})
.strict(),
},
Expand Down
20 changes: 19 additions & 1 deletion web/components/answers/multiple-choice-answers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ExpandingInput } from '../widgets/expanding-input'
import { InfoTooltip } from '../widgets/info-tooltip'
import { OutcomeType } from 'common/contract'
import { ChoicesToggleGroup } from '../widgets/choices-toggle-group'
import { Button } from '../buttons/button'

export function MultipleChoiceAnswers(props: {
answers: string[]
Expand All @@ -15,6 +16,9 @@ export function MultipleChoiceAnswers(props: {
shouldAnswersSumToOne: boolean
outcomeType: OutcomeType
placeholder?: string
question: string
generateAnswers: () => void
isGeneratingAnswers: boolean
}) {
const {
answers,
Expand All @@ -24,6 +28,9 @@ export function MultipleChoiceAnswers(props: {
shouldAnswersSumToOne,
outcomeType,
placeholder,
question,
generateAnswers,
isGeneratingAnswers,
} = props

const setAnswer = (i: number, answer: string) => {
Expand Down Expand Up @@ -105,7 +112,18 @@ export function MultipleChoiceAnswers(props: {
</Row>
)}
{numAnswers < MAX_ANSWERS && (
<Row className="justify-end">
<Row className="justify-end gap-2">
{question && outcomeType === 'MULTIPLE_CHOICE' && (
<Button
color="indigo-outline"
size="xs"
loading={isGeneratingAnswers}
onClick={generateAnswers}
disabled={!question || isGeneratingAnswers}
>
Generate with AI
</Button>
)}
<button
type="button"
onClick={addAnswer}
Expand Down
23 changes: 23 additions & 0 deletions web/components/new-contract/contract-params-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,7 @@ export function ContractParamsForm(props: {
answers,
outcomeType,
shouldAnswersSumToOne,
addAnswersMode,
})
if (result.description && editor) {
const endPos = editor.state.doc.content.size
Expand All @@ -515,6 +516,25 @@ export function ContractParamsForm(props: {
}
setIsGeneratingDescription(false)
}
const [isGeneratingAnswers, setIsGeneratingAnswers] = useState(false)

const generateAnswers = async () => {
if (!question || outcomeType !== 'MULTIPLE_CHOICE') return
setIsGeneratingAnswers(true)
try {
const result = await api('generate-ai-answers', {
question,
description: editor?.getHTML(),
shouldAnswersSumToOne,
answers,
})
setAnswers([...answers, ...result.answers])
setAddAnswersMode(result.addAnswersMode)
} catch (e) {
console.error('Error generating answers:', e)
}
setIsGeneratingAnswers(false)
}

const undoGeneration = () => {
if (preGenerateContent && editor) {
Expand Down Expand Up @@ -557,6 +577,9 @@ export function ContractParamsForm(props: {
shouldAnswersSumToOne={shouldAnswersSumToOne}
outcomeType={outcomeType}
placeholder={isMulti ? 'Type your answer..' : undefined}
question={question}
generateAnswers={generateAnswers}
isGeneratingAnswers={isGeneratingAnswers}
/>
)}
{outcomeType == 'BOUNTIED_QUESTION' && (
Expand Down

0 comments on commit 825deef

Please sign in to comment.