feat: add grounding source info to gemini message
This commit is contained in:
parent
6a5faa6610
commit
2b4cfe7cb1
@ -42,6 +42,9 @@
|
|||||||
--color-active: rgba(55, 55, 55, 1);
|
--color-active: rgba(55, 55, 55, 1);
|
||||||
--color-frame-border: #333;
|
--color-frame-border: #333;
|
||||||
--color-group-background: var(--color-background-soft);
|
--color-group-background: var(--color-background-soft);
|
||||||
|
|
||||||
|
--color-reference: #404040;
|
||||||
|
--color-reference-text: #ffffff;
|
||||||
--color-reference-background: #0b0e12;
|
--color-reference-background: #0b0e12;
|
||||||
|
|
||||||
--navbar-background-mac: rgba(30, 30, 30, 0.6);
|
--navbar-background-mac: rgba(30, 30, 30, 0.6);
|
||||||
@ -102,6 +105,9 @@ body[theme-mode='light'] {
|
|||||||
--color-active: var(--color-white-soft);
|
--color-active: var(--color-white-soft);
|
||||||
--color-frame-border: #ddd;
|
--color-frame-border: #ddd;
|
||||||
--color-group-background: var(--color-white);
|
--color-group-background: var(--color-white);
|
||||||
|
|
||||||
|
--color-reference: #cfe1ff;
|
||||||
|
--color-reference-text: #000000;
|
||||||
--color-reference-background: #f1f7ff;
|
--color-reference-background: #f1f7ff;
|
||||||
|
|
||||||
--navbar-background-mac: rgba(255, 255, 255, 0.6);
|
--navbar-background-mac: rgba(255, 255, 255, 0.6);
|
||||||
|
|||||||
@ -208,6 +208,14 @@
|
|||||||
|
|
||||||
sup {
|
sup {
|
||||||
top: -0.5em;
|
top: -0.5em;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--color-reference);
|
||||||
|
color: var(--color-reference-text);
|
||||||
|
padding: 2px 5px;
|
||||||
|
zoom: 0.8;
|
||||||
|
& > span.link {
|
||||||
|
color: var(--color-white);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sub {
|
sub {
|
||||||
@ -226,51 +234,55 @@
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.footnotes {
|
.footnotes {
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
padding-top: 1em;
|
padding-top: 1em;
|
||||||
|
|
||||||
background-color: var(--color-reference-background);
|
background-color: var(--color-reference-background);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
|
|
||||||
h4 {
|
h4 {
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--color-link);
|
||||||
|
}
|
||||||
|
|
||||||
|
ol {
|
||||||
|
padding-left: 1em;
|
||||||
|
margin: 0;
|
||||||
|
li:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ol {
|
li {
|
||||||
padding-left: 1em;
|
font-size: 0.9em;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
color: var(--color-text-light);
|
||||||
|
|
||||||
|
p {
|
||||||
|
display: inline;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
li:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
li {
|
.footnote-backref {
|
||||||
font-size: 0.9em;
|
font-size: 0.8em;
|
||||||
margin-bottom: 0.5em;
|
vertical-align: super;
|
||||||
color: var(--color-text-light);
|
line-height: 0;
|
||||||
|
margin-left: 5px;
|
||||||
|
color: var(--color-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
p {
|
&:hover {
|
||||||
display: inline;
|
text-decoration: underline;
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.footnote-backref {
|
|
||||||
font-size: 0.8em;
|
|
||||||
vertical-align: super;
|
|
||||||
line-height: 0;
|
|
||||||
margin-left: 5px;
|
|
||||||
color: var(--color-primary);
|
|
||||||
text-decoration: none;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -123,7 +123,6 @@ import YiModelLogo from '@renderer/assets/images/models/yi.png'
|
|||||||
import YiModelLogoDark from '@renderer/assets/images/models/yi_dark.png'
|
import YiModelLogoDark from '@renderer/assets/images/models/yi_dark.png'
|
||||||
import { getProviderByModel } from '@renderer/services/AssistantService'
|
import { getProviderByModel } from '@renderer/services/AssistantService'
|
||||||
import { Model } from '@renderer/types'
|
import { Model } from '@renderer/types'
|
||||||
import { isEmpty } from 'lodash'
|
|
||||||
import OpenAI from 'openai'
|
import OpenAI from 'openai'
|
||||||
|
|
||||||
import { getWebSearchTools } from './tools'
|
import { getWebSearchTools } from './tools'
|
||||||
@ -1087,28 +1086,14 @@ export function isWebSearchModel(model: Model): boolean {
|
|||||||
|
|
||||||
export function getOpenAIWebSearchParams(model: Model): Record<string, any> {
|
export function getOpenAIWebSearchParams(model: Model): Record<string, any> {
|
||||||
if (isWebSearchModel(model)) {
|
if (isWebSearchModel(model)) {
|
||||||
|
const webSearchTools = getWebSearchTools(model)
|
||||||
|
|
||||||
if (model.provider === 'hunyuan') {
|
if (model.provider === 'hunyuan') {
|
||||||
return { enable_enhancement: true }
|
return { enable_enhancement: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (model.provider === 'zhipu') {
|
|
||||||
const webSearchTools = getWebSearchTools(model)
|
|
||||||
return isEmpty(webSearchTools)
|
|
||||||
? {}
|
|
||||||
: {
|
|
||||||
tools: webSearchTools
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tools: [
|
tools: webSearchTools
|
||||||
{
|
|
||||||
type: 'function',
|
|
||||||
function: {
|
|
||||||
name: 'googleSearch'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,29 +1,32 @@
|
|||||||
import { Model } from '@renderer/types'
|
import { Model } from '@renderer/types'
|
||||||
import { ChatCompletionTool } from 'openai/resources'
|
import { ChatCompletionTool } from 'openai/resources'
|
||||||
|
|
||||||
import { isWebSearchModel } from './models'
|
|
||||||
|
|
||||||
export function getWebSearchTools(model: Model): ChatCompletionTool[] {
|
export function getWebSearchTools(model: Model): ChatCompletionTool[] {
|
||||||
if (model && model.provider === 'zhipu') {
|
if (model?.provider === 'zhipu') {
|
||||||
if (isWebSearchModel(model)) {
|
if (model.id === 'glm-4-alltools') {
|
||||||
if (model.id === 'glm-4-alltools') {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
type: 'web_browser'
|
|
||||||
} as unknown as ChatCompletionTool
|
|
||||||
]
|
|
||||||
}
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
type: 'web_search',
|
type: 'web_browser'
|
||||||
web_search: {
|
|
||||||
enable: true,
|
|
||||||
search_result: true
|
|
||||||
}
|
|
||||||
} as unknown as ChatCompletionTool
|
} as unknown as ChatCompletionTool
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: 'web_search',
|
||||||
|
web_search: {
|
||||||
|
enable: true,
|
||||||
|
search_result: true
|
||||||
|
}
|
||||||
|
} as unknown as ChatCompletionTool
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
return []
|
return [
|
||||||
|
{
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'googleSearch'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { DownloadOutlined, ExpandOutlined } from '@ant-design/icons'
|
import { DownloadOutlined, ExpandOutlined } from '@ant-design/icons'
|
||||||
import MinApp from '@renderer/components/MinApp'
|
import MinApp from '@renderer/components/MinApp'
|
||||||
import { AppLogo } from '@renderer/config/env'
|
import { AppLogo } from '@renderer/config/env'
|
||||||
import { extractTitle } from '@renderer/utils/formula'
|
import { extractTitle } from '@renderer/utils/formats'
|
||||||
import { Button } from 'antd'
|
import { Button } from 'antd'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import 'katex/dist/katex.min.css'
|
|||||||
|
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { Message } from '@renderer/types'
|
import { Message } from '@renderer/types'
|
||||||
import { escapeBrackets, removeSvgEmptyLines } from '@renderer/utils/formula'
|
import { escapeBrackets, removeSvgEmptyLines, withGeminiGrounding } from '@renderer/utils/formats'
|
||||||
import { isEmpty } from 'lodash'
|
import { isEmpty } from 'lodash'
|
||||||
import { FC, useMemo } from 'react'
|
import { FC, useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@ -34,9 +34,9 @@ const Markdown: FC<Props> = ({ message }) => {
|
|||||||
const messageContent = useMemo(() => {
|
const messageContent = useMemo(() => {
|
||||||
const empty = isEmpty(message.content)
|
const empty = isEmpty(message.content)
|
||||||
const paused = message.status === 'paused'
|
const paused = message.status === 'paused'
|
||||||
const content = empty && paused ? t('message.chat.completion.paused') : message.content
|
const content = empty && paused ? t('message.chat.completion.paused') : withGeminiGrounding(message)
|
||||||
return removeSvgEmptyLines(escapeBrackets(content))
|
return removeSvgEmptyLines(escapeBrackets(content))
|
||||||
}, [message.content, message.status, t])
|
}, [message, t])
|
||||||
|
|
||||||
const rehypePlugins = useMemo(() => {
|
const rehypePlugins = useMemo(() => {
|
||||||
const hasElements = ALLOWED_ELEMENTS.test(messageContent)
|
const hasElements = ALLOWED_ELEMENTS.test(messageContent)
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import styled from 'styled-components'
|
|||||||
import Markdown from '../Markdown/Markdown'
|
import Markdown from '../Markdown/Markdown'
|
||||||
import MessageAttachments from './MessageAttachments'
|
import MessageAttachments from './MessageAttachments'
|
||||||
import MessageError from './MessageError'
|
import MessageError from './MessageError'
|
||||||
|
import MessageSearchResults from './MessageSearchResults'
|
||||||
|
|
||||||
const MessageContent: React.FC<{
|
const MessageContent: React.FC<{
|
||||||
message: Message
|
message: Message
|
||||||
@ -50,6 +51,7 @@ const MessageContent: React.FC<{
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<MessageAttachments message={message} />
|
<MessageAttachments message={message} />
|
||||||
|
<MessageSearchResults message={message} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,96 @@
|
|||||||
|
import { InfoCircleOutlined } from '@ant-design/icons'
|
||||||
|
import { Message } from '@renderer/types'
|
||||||
|
import { FC } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
message: Message
|
||||||
|
}
|
||||||
|
|
||||||
|
const MessageSearchResults: FC<Props> = ({ message }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
if (!message.metadata?.groundingMetadata) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const { groundingChunks, searchEntryPoint } = message.metadata.groundingMetadata
|
||||||
|
|
||||||
|
if (!groundingChunks) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
let searchEntryContent = searchEntryPoint?.renderedContent
|
||||||
|
|
||||||
|
searchEntryContent = searchEntryContent?.replace(
|
||||||
|
/@media \(prefers-color-scheme: light\)/g,
|
||||||
|
'body[theme-mode="light"]'
|
||||||
|
)
|
||||||
|
|
||||||
|
searchEntryContent = searchEntryContent?.replace(/@media \(prefers-color-scheme: dark\)/g, 'body[theme-mode="dark"]')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Container className="footnotes">
|
||||||
|
<TitleRow>
|
||||||
|
<Title>{t('common.footnotes')}</Title>
|
||||||
|
<InfoCircleOutlined />
|
||||||
|
</TitleRow>
|
||||||
|
<Sources>
|
||||||
|
{groundingChunks.map((chunk, index) => (
|
||||||
|
<SourceItem key={index}>
|
||||||
|
<Link href={chunk.web?.uri} target="_blank" rel="noopener noreferrer">
|
||||||
|
{chunk.web?.title}
|
||||||
|
</Link>
|
||||||
|
</SourceItem>
|
||||||
|
))}
|
||||||
|
</Sources>
|
||||||
|
</Container>
|
||||||
|
<SearchEntryPoint dangerouslySetInnerHTML={{ __html: searchEntryContent || '' }} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
`
|
||||||
|
|
||||||
|
const TitleRow = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Title = styled.h4`
|
||||||
|
margin: 0 !important;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Sources = styled.ol`
|
||||||
|
margin-top: 10px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const SourceItem = styled.li`
|
||||||
|
margin-bottom: 5px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Link = styled.a`
|
||||||
|
margin-left: 5px;
|
||||||
|
color: var(--color-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const SearchEntryPoint = styled.div`
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default MessageSearchResults
|
||||||
@ -176,7 +176,8 @@ export default class GeminiProvider extends BaseProvider {
|
|||||||
completion_tokens: response.usageMetadata?.candidatesTokenCount,
|
completion_tokens: response.usageMetadata?.candidatesTokenCount,
|
||||||
time_completion_millsec,
|
time_completion_millsec,
|
||||||
time_first_token_millsec: 0
|
time_first_token_millsec: 0
|
||||||
}
|
},
|
||||||
|
search: response.candidates?.[0]?.groundingMetadata
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -201,7 +202,8 @@ export default class GeminiProvider extends BaseProvider {
|
|||||||
completion_tokens: chunk.usageMetadata?.candidatesTokenCount,
|
completion_tokens: chunk.usageMetadata?.candidatesTokenCount,
|
||||||
time_completion_millsec,
|
time_completion_millsec,
|
||||||
time_first_token_millsec
|
time_first_token_millsec
|
||||||
}
|
},
|
||||||
|
search: chunk.candidates?.[0]?.groundingMetadata
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4
src/renderer/src/providers/index.d.ts
vendored
4
src/renderer/src/providers/index.d.ts
vendored
@ -1,14 +1,16 @@
|
|||||||
|
import type { GroundingMetadata } from '@google/generative-ai'
|
||||||
import type { Assistant, Metrics } from '@renderer/types'
|
import type { Assistant, Metrics } from '@renderer/types'
|
||||||
|
|
||||||
interface ChunkCallbackData {
|
interface ChunkCallbackData {
|
||||||
text?: string
|
text?: string
|
||||||
usage?: OpenAI.Completions.CompletionUsage
|
usage?: OpenAI.Completions.CompletionUsage
|
||||||
metrics?: Metrics
|
metrics?: Metrics
|
||||||
|
search?: GroundingMetadata
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CompletionsParams {
|
interface CompletionsParams {
|
||||||
messages: Message[]
|
messages: Message[]
|
||||||
assistant: Assistant
|
assistant: Assistant
|
||||||
onChunk: ({ text, usage }: ChunkCallbackData) => void
|
onChunk: ({ text, usage, metrics, search }: ChunkCallbackData) => void
|
||||||
onFilterMessages: (messages: Message[]) => void
|
onFilterMessages: (messages: Message[]) => void
|
||||||
}
|
}
|
||||||
|
|||||||
@ -57,10 +57,15 @@ export async function fetchChatCompletion({
|
|||||||
messages,
|
messages,
|
||||||
assistant,
|
assistant,
|
||||||
onFilterMessages: (messages) => (_messages = messages),
|
onFilterMessages: (messages) => (_messages = messages),
|
||||||
onChunk: ({ text, usage, metrics }) => {
|
onChunk: ({ text, usage, metrics, search }) => {
|
||||||
message.content = message.content + text || ''
|
message.content = message.content + text || ''
|
||||||
message.usage = usage
|
message.usage = usage
|
||||||
message.metrics = metrics
|
message.metrics = metrics
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
message.metadata = { groundingMetadata: search }
|
||||||
|
}
|
||||||
|
|
||||||
onResponse({ ...message, status: 'pending' })
|
onResponse({ ...message, status: 'pending' })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -59,6 +59,10 @@ export type Message = {
|
|||||||
knowledgeBaseIds?: string[]
|
knowledgeBaseIds?: string[]
|
||||||
type: 'text' | '@' | 'clear'
|
type: 'text' | '@' | 'clear'
|
||||||
isPreset?: boolean
|
isPreset?: boolean
|
||||||
|
metadata?: {
|
||||||
|
// Gemini
|
||||||
|
groundingMetadata?: any
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Metrics = {
|
export type Metrics = {
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { Message } from '@renderer/types'
|
||||||
|
|
||||||
export function escapeDollarNumber(text: string) {
|
export function escapeDollarNumber(text: string) {
|
||||||
let escapedText = ''
|
let escapedText = ''
|
||||||
|
|
||||||
@ -56,3 +58,25 @@ export function removeSvgEmptyLines(text: string): string {
|
|||||||
.join('\n')
|
.join('\n')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function withGeminiGrounding(message: Message) {
|
||||||
|
const { groundingSupports } = message?.metadata?.groundingMetadata || {}
|
||||||
|
|
||||||
|
if (!groundingSupports) {
|
||||||
|
return message.content
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = message.content
|
||||||
|
|
||||||
|
groundingSupports.forEach((support) => {
|
||||||
|
const text = support.segment.text
|
||||||
|
const indices = support.groundingChunkIndices
|
||||||
|
const nodes = indices.reduce((acc, index) => {
|
||||||
|
acc.push(`<sup>${index + 1}</sup>`)
|
||||||
|
return acc
|
||||||
|
}, [])
|
||||||
|
content = content.replace(text, `${text} ${nodes.join(' ')}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
return content
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user