mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-11-29 16:43:16 +00:00
Compare commits
13 Commits
0.14.0
...
embeddings
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7140f2cd70 | ||
|
|
b404162364 | ||
|
|
e879ef805f | ||
|
|
a1e6978c8f | ||
|
|
584391dd59 | ||
|
|
0cc22de545 | ||
|
|
aa9d359039 | ||
|
|
cef5731028 | ||
|
|
5bc28bd4fd | ||
|
|
55a1d867c3 | ||
|
|
7bc83caa99 | ||
|
|
3aceca63c6 | ||
|
|
9bc166ffd4 |
@@ -52,8 +52,10 @@
|
||||
- [x] Chatbots menu re-design to handle tools, agent types, and more (April 2025)
|
||||
- [x] New input box in the conversation menu (April 2025)
|
||||
- [x] Add triggerable actions / tools (webhook) (April 2025)
|
||||
- [ ] Anthropic Tool compatibility (May 2025)
|
||||
- [ ] Add OAuth 2.0 authentication for tools and sources
|
||||
- [x] Agent optimisations (May 2025)
|
||||
- [ ] Anthropic Tool compatibility (June 2025)
|
||||
- [ ] MCP support (June 2025)
|
||||
- [ ] Add OAuth 2.0 authentication for tools and sources (July 2025)
|
||||
- [ ] Agent scheduling
|
||||
|
||||
You can find our full roadmap [here](https://github.com/orgs/arc53/projects/2). Please don't hesitate to contribute or create issues, it helps us improve DocsGPT!
|
||||
|
||||
@@ -16,6 +16,7 @@ class Settings(BaseSettings):
|
||||
None # if LLM_PROVIDER is openai, LLM_NAME can be gpt-4 or gpt-3.5-turbo
|
||||
)
|
||||
EMBEDDINGS_NAME: str = "huggingface_sentence-transformers/all-mpnet-base-v2"
|
||||
EMBEDDINGS_PATH: Optional[str] = "./models/all-mpnet-base-v2" # Set None for SentenceTransformer to manage download
|
||||
CELERY_BROKER_URL: str = "redis://localhost:6379/0"
|
||||
CELERY_RESULT_BACKEND: str = "redis://localhost:6379/1"
|
||||
MONGO_URI: str = "mongodb://localhost:27017/docsgpt"
|
||||
|
||||
@@ -14,5 +14,5 @@ class LLMHandlerCreator:
|
||||
def create_handler(cls, llm_type: str, *args, **kwargs) -> LLMHandler:
|
||||
handler_class = cls.handlers.get(llm_type.lower())
|
||||
if not handler_class:
|
||||
raise ValueError(f"No LLM handler class found for type {llm_type}")
|
||||
handler_class = OpenAILLMHandler
|
||||
return handler_class(*args, **kwargs)
|
||||
|
||||
@@ -74,17 +74,11 @@ class BaseVectorStore(ABC):
|
||||
embeddings_name,
|
||||
openai_api_key=embeddings_key
|
||||
)
|
||||
elif embeddings_name == "huggingface_sentence-transformers/all-mpnet-base-v2":
|
||||
if os.path.exists("./models/all-mpnet-base-v2"):
|
||||
embedding_instance = EmbeddingsSingleton.get_instance(
|
||||
embeddings_name = "./models/all-mpnet-base-v2",
|
||||
)
|
||||
else:
|
||||
embedding_instance = EmbeddingsSingleton.get_instance(
|
||||
embeddings_name,
|
||||
)
|
||||
else:
|
||||
embedding_instance = EmbeddingsSingleton.get_instance(embeddings_name)
|
||||
model_identifier = embeddings_name
|
||||
if settings.EMBEDDINGS_PATH and os.path.exists(settings.EMBEDDINGS_PATH):
|
||||
model_identifier = settings.EMBEDDINGS_PATH
|
||||
embedding_instance = EmbeddingsSingleton.get_instance(model_identifier)
|
||||
|
||||
return embedding_instance
|
||||
|
||||
|
||||
@@ -81,8 +81,27 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
useState<ActiveState>('INACTIVE');
|
||||
const [recentAgents, setRecentAgents] = useState<Agent[]>([]);
|
||||
|
||||
const navRef = useRef(null);
|
||||
const navRef = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (
|
||||
navRef.current &&
|
||||
!navRef.current.contains(event.target as Node) &&
|
||||
(isMobile || isTablet) &&
|
||||
navOpen
|
||||
) {
|
||||
setNavOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
//event listener only for mobile/tablet when nav is open
|
||||
if ((isMobile || isTablet) && navOpen) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}
|
||||
}, [navOpen, isMobile, isTablet, setNavOpen]);
|
||||
async function fetchRecentAgents() {
|
||||
try {
|
||||
const response = await userService.getPinnedAgents(token);
|
||||
|
||||
@@ -6,24 +6,23 @@ import ConversationMessages from '../conversation/ConversationMessages';
|
||||
import { Query } from '../conversation/conversationModels';
|
||||
import {
|
||||
addQuery,
|
||||
fetchAnswer,
|
||||
handleAbort,
|
||||
fetchPreviewAnswer,
|
||||
handlePreviewAbort,
|
||||
resendQuery,
|
||||
resetConversation,
|
||||
selectQueries,
|
||||
selectStatus,
|
||||
} from '../conversation/conversationSlice';
|
||||
resetPreview,
|
||||
selectPreviewQueries,
|
||||
selectPreviewStatus,
|
||||
} from './agentPreviewSlice';
|
||||
import { selectSelectedAgent } from '../preferences/preferenceSlice';
|
||||
import { AppDispatch } from '../store';
|
||||
|
||||
export default function AgentPreview() {
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
|
||||
const queries = useSelector(selectQueries);
|
||||
const status = useSelector(selectStatus);
|
||||
const queries = useSelector(selectPreviewQueries);
|
||||
const status = useSelector(selectPreviewStatus);
|
||||
const selectedAgent = useSelector(selectSelectedAgent);
|
||||
|
||||
const [input, setInput] = useState('');
|
||||
const [lastQueryReturnedErr, setLastQueryReturnedErr] = useState(false);
|
||||
|
||||
const fetchStream = useRef<any>(null);
|
||||
@@ -31,7 +30,7 @@ export default function AgentPreview() {
|
||||
const handleFetchAnswer = useCallback(
|
||||
({ question, index }: { question: string; index?: number }) => {
|
||||
fetchStream.current = dispatch(
|
||||
fetchAnswer({ question, indx: index, isPreview: true }),
|
||||
fetchPreviewAnswer({ question, indx: index }),
|
||||
);
|
||||
},
|
||||
[dispatch],
|
||||
@@ -95,11 +94,11 @@ export default function AgentPreview() {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(resetConversation());
|
||||
dispatch(resetPreview());
|
||||
return () => {
|
||||
if (fetchStream.current) fetchStream.current.abort();
|
||||
handleAbort();
|
||||
dispatch(resetConversation());
|
||||
handlePreviewAbort();
|
||||
dispatch(resetPreview());
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
|
||||
@@ -57,9 +57,7 @@ export default function SharedAgent() {
|
||||
|
||||
const handleFetchAnswer = useCallback(
|
||||
({ question, index }: { question: string; index?: number }) => {
|
||||
fetchStream.current = dispatch(
|
||||
fetchAnswer({ question, indx: index, isPreview: false }),
|
||||
);
|
||||
fetchStream.current = dispatch(fetchAnswer({ question, indx: index }));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
319
frontend/src/agents/agentPreviewSlice.ts
Normal file
319
frontend/src/agents/agentPreviewSlice.ts
Normal file
@@ -0,0 +1,319 @@
|
||||
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import {
|
||||
Answer,
|
||||
ConversationState,
|
||||
Query,
|
||||
Status,
|
||||
} from '../conversation/conversationModels';
|
||||
import {
|
||||
handleFetchAnswer,
|
||||
handleFetchAnswerSteaming,
|
||||
} from '../conversation/conversationHandlers';
|
||||
import {
|
||||
selectCompletedAttachments,
|
||||
clearAttachments,
|
||||
} from '../upload/uploadSlice';
|
||||
import store from '../store';
|
||||
|
||||
const initialState: ConversationState = {
|
||||
queries: [],
|
||||
status: 'idle',
|
||||
conversationId: null,
|
||||
};
|
||||
|
||||
const API_STREAMING = import.meta.env.VITE_API_STREAMING === 'true';
|
||||
|
||||
let abortController: AbortController | null = null;
|
||||
export function handlePreviewAbort() {
|
||||
if (abortController) {
|
||||
abortController.abort();
|
||||
abortController = null;
|
||||
}
|
||||
}
|
||||
|
||||
export const fetchPreviewAnswer = createAsyncThunk<
|
||||
Answer,
|
||||
{ question: string; indx?: number }
|
||||
>(
|
||||
'agentPreview/fetchAnswer',
|
||||
async ({ question, indx }, { dispatch, getState }) => {
|
||||
if (abortController) abortController.abort();
|
||||
abortController = new AbortController();
|
||||
const { signal } = abortController;
|
||||
|
||||
const state = getState() as RootState;
|
||||
const attachmentIds = selectCompletedAttachments(state)
|
||||
.filter((a) => a.id)
|
||||
.map((a) => a.id) as string[];
|
||||
|
||||
if (attachmentIds.length > 0) {
|
||||
dispatch(clearAttachments());
|
||||
}
|
||||
|
||||
if (state.preference) {
|
||||
if (API_STREAMING) {
|
||||
await handleFetchAnswerSteaming(
|
||||
question,
|
||||
signal,
|
||||
state.preference.token,
|
||||
state.preference.selectedDocs!,
|
||||
state.agentPreview.queries,
|
||||
null, // No conversation ID for previews
|
||||
state.preference.prompt.id,
|
||||
state.preference.chunks,
|
||||
state.preference.token_limit,
|
||||
(event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
const targetIndex = indx ?? state.agentPreview.queries.length - 1;
|
||||
|
||||
if (data.type === 'end') {
|
||||
dispatch(agentPreviewSlice.actions.setStatus('idle'));
|
||||
} else if (data.type === 'thought') {
|
||||
dispatch(
|
||||
updateThought({
|
||||
index: targetIndex,
|
||||
query: { thought: data.thought },
|
||||
}),
|
||||
);
|
||||
} else if (data.type === 'source') {
|
||||
dispatch(
|
||||
updateStreamingSource({
|
||||
index: targetIndex,
|
||||
query: { sources: data.source ?? [] },
|
||||
}),
|
||||
);
|
||||
} else if (data.type === 'tool_call') {
|
||||
dispatch(
|
||||
updateToolCall({
|
||||
index: targetIndex,
|
||||
tool_call: data.data,
|
||||
}),
|
||||
);
|
||||
} else if (data.type === 'error') {
|
||||
dispatch(agentPreviewSlice.actions.setStatus('failed'));
|
||||
dispatch(
|
||||
agentPreviewSlice.actions.raiseError({
|
||||
index: targetIndex,
|
||||
message: data.error,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
dispatch(
|
||||
updateStreamingQuery({
|
||||
index: targetIndex,
|
||||
query: { response: data.answer },
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
indx,
|
||||
state.preference.selectedAgent?.id,
|
||||
attachmentIds,
|
||||
false, // Don't save preview conversations
|
||||
);
|
||||
} else {
|
||||
// Non-streaming implementation
|
||||
const answer = await handleFetchAnswer(
|
||||
question,
|
||||
signal,
|
||||
state.preference.token,
|
||||
state.preference.selectedDocs!,
|
||||
state.agentPreview.queries,
|
||||
null, // No conversation ID for previews
|
||||
state.preference.prompt.id,
|
||||
state.preference.chunks,
|
||||
state.preference.token_limit,
|
||||
state.preference.selectedAgent?.id,
|
||||
attachmentIds,
|
||||
false, // Don't save preview conversations
|
||||
);
|
||||
|
||||
if (answer) {
|
||||
const sourcesPrepped = answer.sources.map(
|
||||
(source: { title: string }) => {
|
||||
if (source && source.title) {
|
||||
const titleParts = source.title.split('/');
|
||||
return {
|
||||
...source,
|
||||
title: titleParts[titleParts.length - 1],
|
||||
};
|
||||
}
|
||||
return source;
|
||||
},
|
||||
);
|
||||
|
||||
const targetIndex = indx ?? state.agentPreview.queries.length - 1;
|
||||
|
||||
dispatch(
|
||||
updateQuery({
|
||||
index: targetIndex,
|
||||
query: {
|
||||
response: answer.answer,
|
||||
thought: answer.thought,
|
||||
sources: sourcesPrepped,
|
||||
tool_calls: answer.toolCalls,
|
||||
},
|
||||
}),
|
||||
);
|
||||
dispatch(agentPreviewSlice.actions.setStatus('idle'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
conversationId: null,
|
||||
title: null,
|
||||
answer: '',
|
||||
query: question,
|
||||
result: '',
|
||||
thought: '',
|
||||
sources: [],
|
||||
tool_calls: [],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export const agentPreviewSlice = createSlice({
|
||||
name: 'agentPreview',
|
||||
initialState,
|
||||
reducers: {
|
||||
addQuery(state, action: PayloadAction<Query>) {
|
||||
state.queries.push(action.payload);
|
||||
},
|
||||
resendQuery(
|
||||
state,
|
||||
action: PayloadAction<{ index: number; prompt: string; query?: Query }>,
|
||||
) {
|
||||
state.queries = [
|
||||
...state.queries.splice(0, action.payload.index),
|
||||
action.payload,
|
||||
];
|
||||
},
|
||||
updateStreamingQuery(
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
index: number;
|
||||
query: Partial<Query>;
|
||||
}>,
|
||||
) {
|
||||
const { index, query } = action.payload;
|
||||
if (state.status === 'idle') return;
|
||||
|
||||
if (query.response != undefined) {
|
||||
state.queries[index].response =
|
||||
(state.queries[index].response || '') + query.response;
|
||||
}
|
||||
},
|
||||
updateThought(
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
index: number;
|
||||
query: Partial<Query>;
|
||||
}>,
|
||||
) {
|
||||
const { index, query } = action.payload;
|
||||
if (query.thought != undefined) {
|
||||
state.queries[index].thought =
|
||||
(state.queries[index].thought || '') + query.thought;
|
||||
}
|
||||
},
|
||||
updateStreamingSource(
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
index: number;
|
||||
query: Partial<Query>;
|
||||
}>,
|
||||
) {
|
||||
const { index, query } = action.payload;
|
||||
if (!state.queries[index].sources) {
|
||||
state.queries[index].sources = query?.sources;
|
||||
} else if (query.sources) {
|
||||
state.queries[index].sources!.push(...query.sources);
|
||||
}
|
||||
},
|
||||
updateToolCall(state, action) {
|
||||
const { index, tool_call } = action.payload;
|
||||
|
||||
if (!state.queries[index].tool_calls) {
|
||||
state.queries[index].tool_calls = [];
|
||||
}
|
||||
|
||||
const existingIndex = state.queries[index].tool_calls.findIndex(
|
||||
(call) => call.call_id === tool_call.call_id,
|
||||
);
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
const existingCall = state.queries[index].tool_calls[existingIndex];
|
||||
state.queries[index].tool_calls[existingIndex] = {
|
||||
...existingCall,
|
||||
...tool_call,
|
||||
};
|
||||
} else state.queries[index].tool_calls.push(tool_call);
|
||||
},
|
||||
updateQuery(
|
||||
state,
|
||||
action: PayloadAction<{ index: number; query: Partial<Query> }>,
|
||||
) {
|
||||
const { index, query } = action.payload;
|
||||
state.queries[index] = {
|
||||
...state.queries[index],
|
||||
...query,
|
||||
};
|
||||
},
|
||||
setStatus(state, action: PayloadAction<Status>) {
|
||||
state.status = action.payload;
|
||||
},
|
||||
raiseError(
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
index: number;
|
||||
message: string;
|
||||
}>,
|
||||
) {
|
||||
const { index, message } = action.payload;
|
||||
state.queries[index].error = message;
|
||||
},
|
||||
resetPreview: (state) => {
|
||||
state.queries = initialState.queries;
|
||||
state.status = initialState.status;
|
||||
state.conversationId = initialState.conversationId;
|
||||
handlePreviewAbort();
|
||||
},
|
||||
},
|
||||
extraReducers(builder) {
|
||||
builder
|
||||
.addCase(fetchPreviewAnswer.pending, (state) => {
|
||||
state.status = 'loading';
|
||||
})
|
||||
.addCase(fetchPreviewAnswer.rejected, (state, action) => {
|
||||
if (action.meta.aborted) {
|
||||
state.status = 'idle';
|
||||
return state;
|
||||
}
|
||||
state.status = 'failed';
|
||||
state.queries[state.queries.length - 1].error = 'Something went wrong';
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
type RootState = ReturnType<typeof store.getState>;
|
||||
|
||||
export const selectPreviewQueries = (state: RootState) =>
|
||||
state.agentPreview.queries;
|
||||
export const selectPreviewStatus = (state: RootState) =>
|
||||
state.agentPreview.status;
|
||||
|
||||
export const {
|
||||
addQuery,
|
||||
updateQuery,
|
||||
resendQuery,
|
||||
updateStreamingQuery,
|
||||
updateThought,
|
||||
updateStreamingSource,
|
||||
updateToolCall,
|
||||
setStatus,
|
||||
raiseError,
|
||||
resetPreview,
|
||||
} = agentPreviewSlice.actions;
|
||||
|
||||
export default agentPreviewSlice.reducer;
|
||||
@@ -53,7 +53,7 @@ function Dropdown({
|
||||
darkBorderColor?: string;
|
||||
showEdit?: boolean;
|
||||
onEdit?: (value: { name: string; id: string; type: string }) => void;
|
||||
showDelete?: boolean;
|
||||
showDelete?: boolean | ((option: any) => boolean);
|
||||
onDelete?: (value: string) => void;
|
||||
placeholder?: string;
|
||||
placeholderTextColor?: string;
|
||||
@@ -173,8 +173,15 @@ function Dropdown({
|
||||
)}
|
||||
{showDelete && onDelete && (
|
||||
<button
|
||||
onClick={() => onDelete(option.id)}
|
||||
disabled={option.type === 'public'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete?.(typeof option === 'string' ? option : option.id);
|
||||
}}
|
||||
className={`${
|
||||
typeof showDelete === 'function' && !showDelete(option)
|
||||
? 'hidden'
|
||||
: ''
|
||||
} mr-2 h-4 w-4 cursor-pointer hover:opacity-50`}
|
||||
>
|
||||
<img
|
||||
src={Trash}
|
||||
|
||||
@@ -157,12 +157,7 @@ const ConversationBubble = forwardRef<
|
||||
{!isEditClicked && (
|
||||
<>
|
||||
<div className="relative mr-2 flex w-full flex-col">
|
||||
<div
|
||||
style={{
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
className="ml-2 mr-2 flex max-w-full items-start gap-2 whitespace-pre-wrap rounded-[28px] bg-gradient-to-b from-medium-purple to-slate-blue py-[14px] pl-[19px] pr-3 text-sm leading-normal text-white sm:text-base"
|
||||
>
|
||||
<div className="ml-2 mr-2 flex max-w-full items-start gap-2 whitespace-pre-wrap break-words rounded-[28px] bg-gradient-to-b from-medium-purple to-slate-blue px-5 py-4 text-sm leading-normal text-white sm:text-base">
|
||||
<div
|
||||
ref={messageRef}
|
||||
className={`${isQuestionCollapsed ? 'line-clamp-4' : ''} w-full`}
|
||||
@@ -175,7 +170,7 @@ const ConversationBubble = forwardRef<
|
||||
e.stopPropagation();
|
||||
setIsQuestionCollapsed(!isQuestionCollapsed);
|
||||
}}
|
||||
className="rounded-full p-2.5 hover:bg-[#D9D9D933]"
|
||||
className="ml-1 rounded-full p-2 hover:bg-[#D9D9D933]"
|
||||
>
|
||||
<img
|
||||
src={ChevronDown}
|
||||
|
||||
@@ -11,13 +11,7 @@ import {
|
||||
handleFetchAnswer,
|
||||
handleFetchAnswerSteaming,
|
||||
} from './conversationHandlers';
|
||||
import {
|
||||
Answer,
|
||||
Attachment,
|
||||
ConversationState,
|
||||
Query,
|
||||
Status,
|
||||
} from './conversationModels';
|
||||
import { Answer, ConversationState, Query, Status } from './conversationModels';
|
||||
import { ToolCallsType } from './types';
|
||||
|
||||
const initialState: ConversationState = {
|
||||
@@ -38,65 +32,64 @@ export function handleAbort() {
|
||||
|
||||
export const fetchAnswer = createAsyncThunk<
|
||||
Answer,
|
||||
{ question: string; indx?: number; isPreview?: boolean }
|
||||
>(
|
||||
'fetchAnswer',
|
||||
async ({ question, indx, isPreview = false }, { dispatch, getState }) => {
|
||||
if (abortController) abortController.abort();
|
||||
abortController = new AbortController();
|
||||
const { signal } = abortController;
|
||||
{ question: string; indx?: number }
|
||||
>('fetchAnswer', async ({ question, indx }, { dispatch, getState }) => {
|
||||
if (abortController) abortController.abort();
|
||||
abortController = new AbortController();
|
||||
const { signal } = abortController;
|
||||
|
||||
let isSourceUpdated = false;
|
||||
const state = getState() as RootState;
|
||||
const attachmentIds = selectCompletedAttachments(state)
|
||||
.filter((a) => a.id)
|
||||
.map((a) => a.id) as string[];
|
||||
let isSourceUpdated = false;
|
||||
const state = getState() as RootState;
|
||||
const attachmentIds = selectCompletedAttachments(state)
|
||||
.filter((a) => a.id)
|
||||
.map((a) => a.id) as string[];
|
||||
|
||||
if (attachmentIds.length > 0) {
|
||||
dispatch(clearAttachments());
|
||||
}
|
||||
if (attachmentIds.length > 0) {
|
||||
dispatch(clearAttachments());
|
||||
}
|
||||
|
||||
const currentConversationId = state.conversation.conversationId;
|
||||
const conversationIdToSend = isPreview ? null : currentConversationId;
|
||||
const save_conversation = isPreview ? false : true;
|
||||
const currentConversationId = state.conversation.conversationId;
|
||||
|
||||
if (state.preference) {
|
||||
if (API_STREAMING) {
|
||||
await handleFetchAnswerSteaming(
|
||||
question,
|
||||
signal,
|
||||
state.preference.token,
|
||||
state.preference.selectedDocs!,
|
||||
state.conversation.queries,
|
||||
conversationIdToSend,
|
||||
state.preference.prompt.id,
|
||||
state.preference.chunks,
|
||||
state.preference.token_limit,
|
||||
(event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
const targetIndex = indx ?? state.conversation.queries.length - 1;
|
||||
if (state.preference) {
|
||||
if (API_STREAMING) {
|
||||
await handleFetchAnswerSteaming(
|
||||
question,
|
||||
signal,
|
||||
state.preference.token,
|
||||
state.preference.selectedDocs!,
|
||||
state.conversation.queries,
|
||||
currentConversationId,
|
||||
state.preference.prompt.id,
|
||||
state.preference.chunks,
|
||||
state.preference.token_limit,
|
||||
(event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
const targetIndex = indx ?? state.conversation.queries.length - 1;
|
||||
|
||||
// Only process events if they match the current conversation
|
||||
if (currentConversationId === state.conversation.conversationId) {
|
||||
if (data.type === 'end') {
|
||||
dispatch(conversationSlice.actions.setStatus('idle'));
|
||||
if (!isPreview) {
|
||||
getConversations(state.preference.token)
|
||||
.then((fetchedConversations) => {
|
||||
dispatch(setConversations(fetchedConversations));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to fetch conversations: ', error);
|
||||
});
|
||||
}
|
||||
getConversations(state.preference.token)
|
||||
.then((fetchedConversations) => {
|
||||
dispatch(setConversations(fetchedConversations));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to fetch conversations: ', error);
|
||||
});
|
||||
if (!isSourceUpdated) {
|
||||
dispatch(
|
||||
updateStreamingSource({
|
||||
conversationId: currentConversationId,
|
||||
index: targetIndex,
|
||||
query: { sources: [] },
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else if (data.type === 'id') {
|
||||
if (!isPreview) {
|
||||
// Only update the conversationId if it's currently null
|
||||
const currentState = getState() as RootState;
|
||||
if (currentState.conversation.conversationId === null) {
|
||||
dispatch(
|
||||
updateConversationId({
|
||||
query: { conversationId: data.id },
|
||||
@@ -107,6 +100,7 @@ export const fetchAnswer = createAsyncThunk<
|
||||
const result = data.thought;
|
||||
dispatch(
|
||||
updateThought({
|
||||
conversationId: currentConversationId,
|
||||
index: targetIndex,
|
||||
query: { thought: result },
|
||||
}),
|
||||
@@ -115,6 +109,7 @@ export const fetchAnswer = createAsyncThunk<
|
||||
isSourceUpdated = true;
|
||||
dispatch(
|
||||
updateStreamingSource({
|
||||
conversationId: currentConversationId,
|
||||
index: targetIndex,
|
||||
query: { sources: data.source ?? [] },
|
||||
}),
|
||||
@@ -131,6 +126,7 @@ export const fetchAnswer = createAsyncThunk<
|
||||
dispatch(conversationSlice.actions.setStatus('failed'));
|
||||
dispatch(
|
||||
conversationSlice.actions.raiseError({
|
||||
conversationId: currentConversationId,
|
||||
index: targetIndex,
|
||||
message: data.error,
|
||||
}),
|
||||
@@ -138,88 +134,87 @@ export const fetchAnswer = createAsyncThunk<
|
||||
} else {
|
||||
dispatch(
|
||||
updateStreamingQuery({
|
||||
conversationId: currentConversationId,
|
||||
index: targetIndex,
|
||||
query: { response: data.answer },
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
indx,
|
||||
state.preference.selectedAgent?.id,
|
||||
attachmentIds,
|
||||
save_conversation,
|
||||
);
|
||||
} else {
|
||||
const answer = await handleFetchAnswer(
|
||||
question,
|
||||
signal,
|
||||
state.preference.token,
|
||||
state.preference.selectedDocs!,
|
||||
state.conversation.queries,
|
||||
state.conversation.conversationId,
|
||||
state.preference.prompt.id,
|
||||
state.preference.chunks,
|
||||
state.preference.token_limit,
|
||||
state.preference.selectedAgent?.id,
|
||||
attachmentIds,
|
||||
save_conversation,
|
||||
);
|
||||
if (answer) {
|
||||
let sourcesPrepped = [];
|
||||
sourcesPrepped = answer.sources.map((source: { title: string }) => {
|
||||
if (source && source.title) {
|
||||
const titleParts = source.title.split('/');
|
||||
return {
|
||||
...source,
|
||||
title: titleParts[titleParts.length - 1],
|
||||
};
|
||||
}
|
||||
return source;
|
||||
});
|
||||
|
||||
const targetIndex = indx ?? state.conversation.queries.length - 1;
|
||||
|
||||
dispatch(
|
||||
updateQuery({
|
||||
index: targetIndex,
|
||||
query: {
|
||||
response: answer.answer,
|
||||
thought: answer.thought,
|
||||
sources: sourcesPrepped,
|
||||
tool_calls: answer.toolCalls,
|
||||
},
|
||||
}),
|
||||
);
|
||||
if (!isPreview) {
|
||||
dispatch(
|
||||
updateConversationId({
|
||||
query: { conversationId: answer.conversationId },
|
||||
}),
|
||||
);
|
||||
getConversations(state.preference.token)
|
||||
.then((fetchedConversations) => {
|
||||
dispatch(setConversations(fetchedConversations));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to fetch conversations: ', error);
|
||||
});
|
||||
}
|
||||
dispatch(conversationSlice.actions.setStatus('idle'));
|
||||
}
|
||||
},
|
||||
indx,
|
||||
state.preference.selectedAgent?.id,
|
||||
attachmentIds,
|
||||
true, // Always save conversation
|
||||
);
|
||||
} else {
|
||||
const answer = await handleFetchAnswer(
|
||||
question,
|
||||
signal,
|
||||
state.preference.token,
|
||||
state.preference.selectedDocs!,
|
||||
state.conversation.queries,
|
||||
state.conversation.conversationId,
|
||||
state.preference.prompt.id,
|
||||
state.preference.chunks,
|
||||
state.preference.token_limit,
|
||||
state.preference.selectedAgent?.id,
|
||||
attachmentIds,
|
||||
true, // Always save conversation
|
||||
);
|
||||
if (answer) {
|
||||
let sourcesPrepped = [];
|
||||
sourcesPrepped = answer.sources.map((source: { title: string }) => {
|
||||
if (source && source.title) {
|
||||
const titleParts = source.title.split('/');
|
||||
return {
|
||||
...source,
|
||||
title: titleParts[titleParts.length - 1],
|
||||
};
|
||||
}
|
||||
return source;
|
||||
});
|
||||
|
||||
const targetIndex = indx ?? state.conversation.queries.length - 1;
|
||||
|
||||
dispatch(
|
||||
updateQuery({
|
||||
index: targetIndex,
|
||||
query: {
|
||||
response: answer.answer,
|
||||
thought: answer.thought,
|
||||
sources: sourcesPrepped,
|
||||
tool_calls: answer.toolCalls,
|
||||
},
|
||||
}),
|
||||
);
|
||||
dispatch(
|
||||
updateConversationId({
|
||||
query: { conversationId: answer.conversationId },
|
||||
}),
|
||||
);
|
||||
getConversations(state.preference.token)
|
||||
.then((fetchedConversations) => {
|
||||
dispatch(setConversations(fetchedConversations));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to fetch conversations: ', error);
|
||||
});
|
||||
dispatch(conversationSlice.actions.setStatus('idle'));
|
||||
}
|
||||
}
|
||||
return {
|
||||
conversationId: null,
|
||||
title: null,
|
||||
answer: '',
|
||||
query: question,
|
||||
result: '',
|
||||
thought: '',
|
||||
sources: [],
|
||||
tool_calls: [],
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
return {
|
||||
conversationId: null,
|
||||
title: null,
|
||||
answer: '',
|
||||
query: question,
|
||||
result: '',
|
||||
thought: '',
|
||||
sources: [],
|
||||
tool_calls: [],
|
||||
};
|
||||
});
|
||||
|
||||
export const conversationSlice = createSlice({
|
||||
name: 'conversation',
|
||||
@@ -242,18 +237,20 @@ export const conversationSlice = createSlice({
|
||||
},
|
||||
updateStreamingQuery(
|
||||
state,
|
||||
action: PayloadAction<{ index: number; query: Partial<Query> }>,
|
||||
action: PayloadAction<{
|
||||
conversationId: string | null;
|
||||
index: number;
|
||||
query: Partial<Query>;
|
||||
}>,
|
||||
) {
|
||||
if (state.status === 'idle') return;
|
||||
const { index, query } = action.payload;
|
||||
const { conversationId, index, query } = action.payload;
|
||||
// Only update if this update is for the current conversation
|
||||
if (state.status === 'idle' || state.conversationId !== conversationId)
|
||||
return;
|
||||
|
||||
if (query.response != undefined) {
|
||||
state.queries[index].response =
|
||||
(state.queries[index].response || '') + query.response;
|
||||
} else {
|
||||
state.queries[index] = {
|
||||
...state.queries[index],
|
||||
...query,
|
||||
};
|
||||
}
|
||||
},
|
||||
updateConversationId(
|
||||
@@ -265,28 +262,35 @@ export const conversationSlice = createSlice({
|
||||
},
|
||||
updateThought(
|
||||
state,
|
||||
action: PayloadAction<{ index: number; query: Partial<Query> }>,
|
||||
action: PayloadAction<{
|
||||
conversationId: string | null;
|
||||
index: number;
|
||||
query: Partial<Query>;
|
||||
}>,
|
||||
) {
|
||||
const { index, query } = action.payload;
|
||||
const { conversationId, index, query } = action.payload;
|
||||
if (state.conversationId !== conversationId) return;
|
||||
|
||||
if (query.thought != undefined) {
|
||||
state.queries[index].thought =
|
||||
(state.queries[index].thought || '') + query.thought;
|
||||
} else {
|
||||
state.queries[index] = {
|
||||
...state.queries[index],
|
||||
...query,
|
||||
};
|
||||
}
|
||||
},
|
||||
updateStreamingSource(
|
||||
state,
|
||||
action: PayloadAction<{ index: number; query: Partial<Query> }>,
|
||||
action: PayloadAction<{
|
||||
conversationId: string | null;
|
||||
index: number;
|
||||
query: Partial<Query>;
|
||||
}>,
|
||||
) {
|
||||
const { index, query } = action.payload;
|
||||
const { conversationId, index, query } = action.payload;
|
||||
if (state.conversationId !== conversationId) return;
|
||||
|
||||
if (!state.queries[index].sources) {
|
||||
state.queries[index].sources = query?.sources;
|
||||
} else {
|
||||
state.queries[index].sources!.push(query.sources![0]);
|
||||
} else if (query.sources) {
|
||||
state.queries[index].sources!.push(...query.sources);
|
||||
}
|
||||
},
|
||||
updateToolCall(state, action) {
|
||||
@@ -323,9 +327,15 @@ export const conversationSlice = createSlice({
|
||||
},
|
||||
raiseError(
|
||||
state,
|
||||
action: PayloadAction<{ index: number; message: string }>,
|
||||
action: PayloadAction<{
|
||||
conversationId: string | null;
|
||||
index: number;
|
||||
message: string;
|
||||
}>,
|
||||
) {
|
||||
const { index, message } = action.payload;
|
||||
const { conversationId, index, message } = action.payload;
|
||||
if (state.conversationId !== conversationId) return;
|
||||
|
||||
state.queries[index].error = message;
|
||||
},
|
||||
|
||||
|
||||
@@ -177,7 +177,7 @@ export default function Prompts({
|
||||
rounded="3xl"
|
||||
border="border"
|
||||
showEdit
|
||||
showDelete
|
||||
showDelete={(prompt) => prompt.type !== 'public'}
|
||||
onEdit={({
|
||||
id,
|
||||
name,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
prefSlice,
|
||||
} from './preferences/preferenceSlice';
|
||||
import uploadReducer from './upload/uploadSlice';
|
||||
import agentPreviewReducer from './agents/agentPreviewSlice';
|
||||
|
||||
const key = localStorage.getItem('DocsGPTApiKey');
|
||||
const prompt = localStorage.getItem('DocsGPTPrompt');
|
||||
@@ -54,6 +55,7 @@ const store = configureStore({
|
||||
conversation: conversationSlice.reducer,
|
||||
sharedConversation: sharedConversationSlice.reducer,
|
||||
upload: uploadReducer,
|
||||
agentPreview: agentPreviewReducer,
|
||||
},
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware().concat(prefListenerMiddleware.middleware),
|
||||
|
||||
Reference in New Issue
Block a user