Skip to content

Commit

Permalink
Support sidebar Max AI - not yet integrated
Browse files Browse the repository at this point in the history
  • Loading branch information
slshults committed Dec 11, 2024
1 parent 1c51c9e commit 55b8ac9
Show file tree
Hide file tree
Showing 15 changed files with 16,239 additions and 11,979 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,7 @@ plugin-transpiler/dist
# Ignore any log files that happen to be present
*.log
# pyright config (keep this until we have a standardized one)
pyrightconfig.json
pyrightconfig.json
sidebar-max-unintegrated/max-venv/
sidebar-max-unintegrated/.vscode
sidebar-max-unintegrated/.vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,16 @@ import { urls } from 'scenes/urls'
import { AvailableFeature, ProductKey, SidePanelTab } from '~/types'

import AlgoliaSearch from '../../components/AlgoliaSearch'
import { SidePanelPaneHeader } from '../components/SidePanelPaneHeader'
import { SIDE_PANEL_TABS } from '../SidePanel'
import { MaxChatInterface } from './sidePanelMaxChatInterface'
{
/* the next two imports are on hold until after MVP */
}
{
/*import { SidePanelPaneHeader } from '../components/SidePanelPaneHeader'*/
}
{
/*import { SIDE_PANEL_TABS } from '../SidePanel'*/
}
import { sidePanelStateLogic } from '../sidePanelStateLogic'
import { sidePanelStatusLogic } from './sidePanelStatusLogic'

Expand Down Expand Up @@ -166,19 +174,50 @@ export const SidePanelSupport = (): JSX.Element => {
const { status } = useValues(sidePanelStatusLogic)

const theLogic = supportLogic({ onClose: () => closeSidePanel(SidePanelTab.Support) })
const { openEmailForm, closeEmailForm } = useActions(theLogic)
const { title, isEmailFormOpen } = useValues(theLogic)
const { openEmailForm, closeEmailForm, openMaxChatInterface, closeMaxChatInterface } = useActions(theLogic)
const { isEmailFormOpen, isMaxChatInterfaceOpen } = useValues(theLogic)

const region = preflight?.region

return (
<>
<SidePanelPaneHeader title={isEmailFormOpen ? title : SIDE_PANEL_TABS[SidePanelTab.Support].label} />
{/* this is on hold until after MVP */}
{/* <SidePanelPaneHeader title={isEmailFormOpen ? title : SIDE_PANEL_TABS[SidePanelTab.Support].label}>
{isMaxChatInterfaceOpen && (
<>
<div className="flex-1" />
<LemonButton
size="small"
sideIcon={<IconExternal />}
targetBlank
onClick={() => {
window.open('/max', '_blank')?.focus()
closeSidePanel(SidePanelTab.Support)
}}
>
Open in new tab
</LemonButton>
</>
)}
</SidePanelPaneHeader> */}

<div className="overflow-y-auto" data-attr="side-panel-support-container">
<div className="p-3 max-w-160 w-full mx-auto">
{isEmailFormOpen ? (
<SupportFormBlock onCancel={() => closeEmailForm()} />
) : isMaxChatInterfaceOpen ? (
<div className="space-y-4">
<MaxChatInterface />
<LemonButton
type="secondary"
onClick={() => closeMaxChatInterface()}
fullWidth
center
className="mt-2"
>
End Chat
</LemonButton>
</div>
) : (
<>
<Section title="Search docs & community questions">
Expand Down Expand Up @@ -230,8 +269,29 @@ export const SidePanelSupport = (): JSX.Element => {
</Section>
) : null}

{/* only allow opening tickets on our Cloud instances */}
{isCloud ? (
{isCloud || true ? (
<Section title="Ask Max the Hedgehog">
<p>
Max is PostHog's support AI who can answer support questions, help you with
troubleshooting, find info in our documentation, write HogQL queries, regex
expressions, etc.
</p>
<LemonButton
type="primary"
fullWidth
center
onClick={() => {
openMaxChatInterface()
}}
targetBlank={false}
className="mt-2"
>
Chat with Max 🦔
</LemonButton>
</Section>
) : null}

{isCloud || true ? (
<Section title="Contact us">
<p>Can't find what you need in the docs?</p>
<LemonButton
Expand All @@ -246,6 +306,7 @@ export const SidePanelSupport = (): JSX.Element => {
</LemonButton>
</Section>
) : null}

<Section title="Ask the community">
<p>
Questions about features, how-tos, or use cases? There are thousands of discussions
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { actions, kea, path, reducers } from 'kea'
import { loaders } from 'kea-loaders'

import type { sidePanelMaxAILogicType } from './sidePanelMaxAILogicType'
import { sidePanelMaxAPI } from './sidePanelMaxAPI'

export interface ChatMessage {
role: 'user' | 'assistant'
content: string
timestamp: string
isRateLimited?: boolean
}

interface MaxResponse {
content: string | { text: string; type: string }
isRateLimited?: boolean
}

export const sidePanelMaxAILogic = kea<sidePanelMaxAILogicType>([
path(['scenes', 'navigation', 'sidepanel', 'sidePanelMaxAILogic']),

actions({
submitMessage: (message: string) => ({ message }),
clearChatHistory: true,
appendAssistantMessage: (content: string) => ({ content }),
setSearchingThinking: (isSearching: boolean) => ({ isSearching }),
setRateLimited: (isLimited: boolean) => ({ isLimited }),
}),

reducers({
currentMessages: [
[] as ChatMessage[],
{
submitMessage: (state, { message }) =>
message.trim()
? [
...state,
{
role: 'user',
content: message,
timestamp: new Date().toISOString(),
},
]
: state,
appendAssistantMessage: (state, { content }) => [
...state,
{
role: 'assistant',
content,
timestamp: new Date().toISOString(),
isRateLimited: content.includes('Rate limit exceeded') || content.includes('rate-limited'),
},
],
clearChatHistory: () => [],
},
],
isSearchingThinking: [
false,
{
setSearchingThinking: (_, { isSearching }) => isSearching,
},
],
isRateLimited: [
false,
{
setRateLimited: (_, { isLimited }) => isLimited,
},
],
}),

loaders(({ actions, values }) => ({
assistantResponse: [
null as string | null,
{
submitMessage: async ({ message }, breakpoint) => {
try {
actions.setSearchingThinking(true)
if (!values.isRateLimited) {
actions.setRateLimited(false)
}
const response = (await sidePanelMaxAPI.sendMessage(message)) as MaxResponse
await breakpoint(100)

const content = typeof response.content === 'string' ? response.content : response.content.text

if (response.isRateLimited) {
actions.setRateLimited(true)
} else {
actions.setRateLimited(false)
}

actions.appendAssistantMessage(content)
return content
} catch (error: unknown) {
if (
error &&
typeof error === 'object' &&
'message' in error &&
typeof error.message === 'string' &&
(error.message.includes('429') || error.message.includes('rate limit'))
) {
actions.setRateLimited(true)
// Keep searching state true while rate limited
} else {
actions.setSearchingThinking(false)
actions.setRateLimited(false)
}
console.error('Error sending message:', error)
return null
}
},
},
],
})),
])
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const MAX_API_HOST = 'http://localhost:3000' // Default port used in sidebar_max_AI.py

let currentSessionId: string | null = null

export const sidePanelMaxAPI = {
async sendMessage(message: string): Promise<{ content: string }> {
const response = await fetch(`${MAX_API_HOST}/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
message,
role: 'user',
session_id: currentSessionId,
}),
})

if (!response.ok) {
throw new Error('Failed to send message to Max')
}

const data = await response.json()
currentSessionId = data.session_id // Store the session ID for next request
return { content: data.content }
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
.SidePanelMaxChatInterface {
&__label {
margin-top: 0.5rem;
margin-right: 0.5rem;
font-size: 0.875rem;
color: var(--muted);
white-space: nowrap;
}

&__message-container {
display: flex;
width: 100%;

&--user {
justify-content: flex-end;
}

&--assistant {
justify-content: flex-start;
}
}

&__message-content {
width: 90%;
min-width: 90%;
max-width: 90%;
padding: 0.5rem;
word-break: break-word;
border-radius: 0.5rem;

&--assistant {
color: var(--default);
background: var(--bg-light);

.dark & {
background: var(--bg-depth);
}
}

&--user {
color: var(--default);
background: var(--bg-light);

.dark & {
background: var(--bg-side);
}
}
}

code {
padding: 0.125rem 0.25rem;
font-family: var(--font-mono);
font-size: 0.75rem;
background: color-mix(in sRGB, var(--bg-light) 85%, var(--primary) 15%);
border-radius: 0.25rem;

.dark & {
background: color-mix(in sRGB, var(--bg-depth) 85%, var(--primary) 15%);
}
}
}
Loading

1 comment on commit 55b8ac9

@slshults
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Max is not yet integrated into the platform in this commit. If you'd like to try running him locally, the steps below should work (assumes you already have python3 and pip3 installed.):

After spinning up docker containers, migrate and start, in a new terminal cd to /posthog/sidebar-max-unintegrated/ then:

pip3 install -r requirements.txt

If you hit errors with that, try this instead:

`pip3 install --user -r requirements.txt`

Next:

cp dot.env.example .env

Then edit .env and drop an Anthropic API key into it.

Then do:

source max-venv/bin/activate

Then run it!

python3 sidebar_max_AI.py 

In a browser on your local, open the help sidebar, click the Chat with Max button, and have a chat. 😊 🦔 ✌️

Please sign in to comment.