Compare commits

..

5 Commits

Author SHA1 Message Date
pablodanswer
2e8493b837 nit 2025-02-10 14:43:16 -08:00
pablodanswer
b467e29179 quick cleanup 2025-02-10 14:36:56 -08:00
pablodanswer
8a905571b0 nit 2025-02-10 14:26:42 -08:00
pablodanswer
1fa34c93ea update 2025-02-10 13:54:59 -08:00
pablodanswer
4b737e9101 improve logging + query history 2025-02-10 13:24:37 -08:00
16 changed files with 150 additions and 107 deletions

View File

@@ -13,6 +13,7 @@ def make_persona_private(
persona_id: int,
user_ids: list[UUID] | None,
group_ids: list[int] | None,
creator_id: UUID | None,
db_session: Session,
) -> None:
db_session.query(Persona__User).filter(
@@ -26,14 +27,15 @@ def make_persona_private(
for user_uuid in user_ids:
db_session.add(Persona__User(persona_id=persona_id, user_id=user_uuid))
create_notification(
user_id=user_uuid,
notif_type=NotificationType.PERSONA_SHARED,
db_session=db_session,
additional_data=PersonaSharedNotificationData(
persona_id=persona_id,
).model_dump(),
)
if creator_id and creator_id != user_uuid:
create_notification(
user_id=user_uuid,
notif_type=NotificationType.PERSONA_SHARED,
db_session=db_session,
additional_data=PersonaSharedNotificationData(
persona_id=persona_id,
).model_dump(),
)
if group_ids:
for group_id in group_ids:
db_session.add(

View File

@@ -132,8 +132,13 @@ def fetch_chat_sessions_eagerly_by_time(
end: datetime,
db_session: Session,
limit: int | None = 500,
offset: int | None = 0,
initial_time: datetime | None = None,
) -> list[ChatSession]:
"""
Fetch chunks of ChatSession objects eagerly by time, supporting pagination
with limit/offset.
"""
time_order: UnaryExpression = desc(ChatSession.time_created)
message_order: UnaryExpression = asc(ChatMessage.id)
@@ -150,6 +155,7 @@ def fetch_chat_sessions_eagerly_by_time(
.order_by(ChatSession.id, time_order)
.distinct(ChatSession.id)
.limit(limit)
.offset(offset)
.subquery()
)
@@ -167,6 +173,4 @@ def fetch_chat_sessions_eagerly_by_time(
.order_by(time_order, message_order)
)
chat_sessions = query.all()
return chat_sessions
return query.all()

View File

@@ -1,5 +1,6 @@
import csv
import io
from collections.abc import Generator
from datetime import datetime
from datetime import timezone
from uuid import UUID
@@ -35,6 +36,8 @@ from onyx.server.query_and_chat.models import ChatSessionsResponse
router = APIRouter()
CHUNK_SIZE = 500 # or whatever size is appropriate
def fetch_and_process_chat_session_history(
db_session: Session,
@@ -203,34 +206,61 @@ def get_query_history_as_csv(
end: datetime | None = None,
db_session: Session = Depends(get_session),
) -> StreamingResponse:
complete_chat_session_history = fetch_and_process_chat_session_history(
db_session=db_session,
start=start or datetime.fromtimestamp(0, tz=timezone.utc),
end=end or datetime.now(tz=timezone.utc),
feedback_type=None,
limit=None,
)
"""Stream the CSV in chunks to avoid timeout and high memory usage."""
# Set up the time range defaults
start_time = start or datetime.fromtimestamp(0, tz=timezone.utc)
end_time = end or datetime.now(tz=timezone.utc)
question_answer_pairs: list[QuestionAnswerPairSnapshot] = []
for chat_session_snapshot in complete_chat_session_history:
question_answer_pairs.extend(
QuestionAnswerPairSnapshot.from_chat_session_snapshot(chat_session_snapshot)
)
# Build the streaming generator
def generate_csv() -> Generator[str, None, None]:
# Prepare CSV writer
fieldnames = list(QuestionAnswerPairSnapshot.model_fields.keys())
buffer = io.StringIO()
writer = csv.DictWriter(buffer, fieldnames=fieldnames)
# Create an in-memory text stream
stream = io.StringIO()
writer = csv.DictWriter(
stream, fieldnames=list(QuestionAnswerPairSnapshot.model_fields.keys())
)
writer.writeheader()
for row in question_answer_pairs:
writer.writerow(row.to_json())
writer.writeheader()
yield buffer.getvalue()
buffer.seek(0)
buffer.truncate(0)
# Reset the stream's position to the start
stream.seek(0)
# Paginate or chunk the fetching of sessions
offset = 0
while True:
# Example chunk-based fetch from DB
chunk_of_chat_sessions = fetch_chat_sessions_eagerly_by_time(
start=start_time,
end=end_time,
db_session=db_session,
limit=CHUNK_SIZE,
offset=offset,
)
if not chunk_of_chat_sessions:
break # no more data
# Convert each chat session to question-answer pairs
for chat_session in chunk_of_chat_sessions:
chat_snapshot = snapshot_from_chat_session(
chat_session=chat_session,
db_session=db_session,
)
if chat_snapshot is None:
continue
# Build Q/A pairs
qa_pairs = QuestionAnswerPairSnapshot.from_chat_session_snapshot(
chat_snapshot
)
for row in qa_pairs:
writer.writerow(row.to_json())
# Yield the CSV row text
yield buffer.getvalue()
buffer.seek(0)
buffer.truncate(0)
offset += CHUNK_SIZE
return StreamingResponse(
iter([stream.getvalue()]),
generate_csv(),
media_type="text/csv",
headers={"Content-Disposition": "attachment;filename=onyx_query_history.csv"},
)

View File

@@ -156,6 +156,8 @@ class DynamicTenantScheduler(PersistentScheduler):
logger.info(
"_try_updating_schedule: Current schedule is up to date, no changes needed"
)
logger.info("Current schedule is the following:")
logger.info(current_schedule)
return
logger.info(

View File

@@ -68,6 +68,8 @@ logger = setup_logger()
def check_for_indexing(self: Task, *, tenant_id: str | None) -> int | None:
"""a lightweight task used to kick off indexing tasks.
Occcasionally does some validation of existing state to clear up error conditions"""
task = f"check_for_indexing start: tenant_id={tenant_id}"
time_start = time.monotonic()
tasks_created = 0
@@ -86,8 +88,11 @@ def check_for_indexing(self: Task, *, tenant_id: str | None) -> int | None:
# these tasks should never overlap
if not lock_beat.acquire(blocking=False):
task_logger.info(f"{task} - Lock not acquired")
return None
task_logger.info(f"{task} - Lock acquired")
try:
locked = True

View File

@@ -234,6 +234,8 @@ def cloud_beat_task_generator(
expires: int = BEAT_EXPIRES_DEFAULT,
) -> bool | None:
"""a lightweight task used to kick off individual beat tasks per tenant."""
task = f"cloud_beat_task_generator start: task_name={task_name}, queue={queue}, priority={priority}"
time_start = time.monotonic()
redis_client = get_redis_client(tenant_id=ONYX_CLOUD_TENANT_ID)
@@ -245,8 +247,10 @@ def cloud_beat_task_generator(
# these tasks should never overlap
if not lock_beat.acquire(blocking=False):
task_logger.info(f"{task} - Lock not acquired")
return None
task_logger.info(f"{task} - Lock acquired")
last_lock_time = time.monotonic()
tenant_ids: list[str] | list[None] = []
@@ -262,6 +266,9 @@ def cloud_beat_task_generator(
if IGNORED_SYNCING_TENANT_LIST and tenant_id in IGNORED_SYNCING_TENANT_LIST:
continue
task_logger.info(
f"{task} - sending task: task_name={task_name}, tenant_id={tenant_id}"
)
self.app.send_task(
task_name,
kwargs=dict(

View File

@@ -163,6 +163,7 @@ def make_persona_private(
persona_id: int,
user_ids: list[UUID] | None,
group_ids: list[int] | None,
creator_id: UUID | None,
db_session: Session,
) -> None:
if user_ids is not None:
@@ -173,14 +174,15 @@ def make_persona_private(
for user_uuid in user_ids:
db_session.add(Persona__User(persona_id=persona_id, user_id=user_uuid))
create_notification(
user_id=user_uuid,
notif_type=NotificationType.PERSONA_SHARED,
db_session=db_session,
additional_data=PersonaSharedNotificationData(
persona_id=persona_id,
).model_dump(),
)
if creator_id and creator_id != user_uuid:
create_notification(
user_id=user_uuid,
notif_type=NotificationType.PERSONA_SHARED,
db_session=db_session,
additional_data=PersonaSharedNotificationData(
persona_id=persona_id,
).model_dump(),
)
db_session.commit()
@@ -240,6 +242,7 @@ def create_update_persona(
persona_id=persona.id,
user_ids=create_persona_request.users,
group_ids=create_persona_request.groups,
creator_id=user.id if user else None,
db_session=db_session,
)
@@ -275,6 +278,7 @@ def update_persona_shared_users(
persona_id=persona_id,
user_ids=user_ids,
group_ids=None,
creator_id=user.id if user else None,
db_session=db_session,
)

View File

@@ -27,7 +27,6 @@ from langchain_core.prompt_values import PromptValue
from onyx.configs.app_configs import LOG_DANSWER_MODEL_INTERACTIONS
from onyx.configs.app_configs import MOCK_LLM_RESPONSE
from onyx.configs.chat_configs import QA_TIMEOUT
from onyx.configs.model_configs import (
DISABLE_LITELLM_STREAMING,
)
@@ -36,7 +35,6 @@ from onyx.configs.model_configs import LITELLM_EXTRA_BODY
from onyx.llm.interfaces import LLM
from onyx.llm.interfaces import LLMConfig
from onyx.llm.interfaces import ToolChoiceOptions
from onyx.llm.utils import model_is_reasoning_model
from onyx.server.utils import mask_string
from onyx.utils.logger import setup_logger
from onyx.utils.long_term_log import LongTermLogger
@@ -231,15 +229,15 @@ class DefaultMultiLLM(LLM):
def __init__(
self,
api_key: str | None,
timeout: int,
model_provider: str,
model_name: str,
timeout: int | None = None,
api_base: str | None = None,
api_version: str | None = None,
deployment_name: str | None = None,
max_output_tokens: int | None = None,
custom_llm_provider: str | None = None,
temperature: float | None = None,
temperature: float = GEN_AI_TEMPERATURE,
custom_config: dict[str, str] | None = None,
extra_headers: dict[str, str] | None = None,
extra_body: dict | None = LITELLM_EXTRA_BODY,
@@ -247,16 +245,9 @@ class DefaultMultiLLM(LLM):
long_term_logger: LongTermLogger | None = None,
):
self._timeout = timeout
if timeout is None:
if model_is_reasoning_model(model_name):
self._timeout = QA_TIMEOUT * 10 # Reasoning models are slow
else:
self._timeout = QA_TIMEOUT
self._temperature = GEN_AI_TEMPERATURE if temperature is None else temperature
self._model_provider = model_provider
self._model_version = model_name
self._temperature = temperature
self._api_key = api_key
self._deployment_name = deployment_name
self._api_base = api_base

View File

@@ -2,6 +2,7 @@ from typing import Any
from onyx.chat.models import PersonaOverrideConfig
from onyx.configs.app_configs import DISABLE_GENERATIVE_AI
from onyx.configs.chat_configs import QA_TIMEOUT
from onyx.configs.model_configs import GEN_AI_MODEL_FALLBACK_MAX_TOKENS
from onyx.configs.model_configs import GEN_AI_TEMPERATURE
from onyx.db.engine import get_session_context_manager
@@ -87,8 +88,8 @@ def get_llms_for_persona(
def get_default_llms(
timeout: int | None = None,
temperature: float | None = None,
timeout: int = QA_TIMEOUT,
temperature: float = GEN_AI_TEMPERATURE,
additional_headers: dict[str, str] | None = None,
long_term_logger: LongTermLogger | None = None,
) -> tuple[LLM, LLM]:
@@ -137,7 +138,7 @@ def get_llm(
api_version: str | None = None,
custom_config: dict[str, str] | None = None,
temperature: float | None = None,
timeout: int | None = None,
timeout: int = QA_TIMEOUT,
additional_headers: dict[str, str] | None = None,
long_term_logger: LongTermLogger | None = None,
) -> LLM:

View File

@@ -29,11 +29,11 @@ OPENAI_PROVIDER_NAME = "openai"
OPEN_AI_MODEL_NAMES = [
"o3-mini",
"o1-mini",
"o1",
"o1-preview",
"o1-2024-12-17",
"gpt-4",
"gpt-4o",
"gpt-4o-mini",
"o1-preview",
"gpt-4-turbo",
"gpt-4-turbo-preview",
"gpt-4-1106-preview",

View File

@@ -543,14 +543,3 @@ def model_supports_image_input(model_name: str, model_provider: str) -> bool:
f"Failed to get model object for {model_provider}/{model_name}"
)
return False
def model_is_reasoning_model(model_name: str) -> bool:
_REASONING_MODEL_NAMES = [
"o1",
"o1-mini",
"o3-mini",
"deepseek-reasoner",
"deepseek-r1",
]
return model_name.lower() in _REASONING_MODEL_NAMES

View File

@@ -147,8 +147,8 @@ const AssistantCard: React.FC<{
)}
</div>
{isOwnedByUser && (
<div className="flex ml-2 relative items-center gap-x-2">
<Popover>
<div className="flex ml-2 relative items-center gap-x-2">
<Popover modal>
<PopoverTrigger>
<button
type="button"
@@ -157,7 +157,9 @@ const AssistantCard: React.FC<{
<FiMoreHorizontal size={16} />
</button>
</PopoverTrigger>
<PopoverContent className={`w-32 z-[10000] p-2`}>
<PopoverContent
className={`w-32 z-[10000] p-2 hover:bg-red-400`}
>
<div className="flex flex-col text-sm space-y-1">
<button
onClick={isOwnedByUser ? handleEdit : undefined}

View File

@@ -8,7 +8,6 @@ import { useUser } from "@/components/user/UserProvider";
import { FilterIcon } from "lucide-react";
import { checkUserOwnsAssistant } from "@/lib/assistants/checkOwnership";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { Modal } from "@/components/Modal";
export const AssistantBadgeSelector = ({
text,
@@ -24,8 +23,8 @@ export const AssistantBadgeSelector = ({
className={`
select-none ${
selected
? "bg-neutral-900 text-white"
: "bg-transparent text-neutral-900"
? "bg-background-900 text-white"
: "bg-transparent text-text-900"
} w-12 h-5 text-center px-1 py-0.5 rounded-lg cursor-pointer text-[12px] font-normal leading-[10px] border border-black justify-center items-center gap-1 inline-flex`}
onClick={toggleFilter}
>
@@ -117,9 +116,9 @@ export function AssistantModal({
);
return (
<div className="fixed inset-0 bg-neutral-950/80 bg-opacity-50 flex items-center justify-center z-50">
<div
className="p-0 max-w-4xl overflow-hidden max-h-[80vh] w-[95%] bg-background rounded-md shadow-2xl transform transition-all duration-300 ease-in-out relative w-11/12 max-w-4xl pt-10 pb-10 px-10 overflow-hidden flex flex-col"
<Dialog open={true} onOpenChange={(open) => !open && hideModal()}>
<DialogContent
className="p-0 max-h-[80vh] max-w-none w-[95%] bg-background rounded-sm shadow-2xl transform transition-all duration-300 ease-in-out relative w-11/12 max-w-4xl pt-10 pb-10 px-10 flex flex-col max-w-4xl"
style={{
position: "fixed",
top: "10vh",
@@ -132,11 +131,11 @@ export function AssistantModal({
<div className="flex flex-col sticky top-0 z-10">
<div className="flex px-2 justify-between items-center gap-x-2 mb-0">
<div className="h-12 w-full rounded-lg flex-col justify-center items-start gap-2.5 inline-flex">
<div className="h-12 rounded-md w-full shadow-[0px_0px_2px_0px_rgba(0,0,0,0.25)] border border-[#dcdad4] flex items-center px-3">
<div className="h-12 rounded-md w-full shadow-[0px_0px_2px_0px_rgba(0,0,0,0.25)] border border-background-300 flex items-center px-3">
{!isSearchFocused && (
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5 text-gray-400"
className="h-5 w-5 text-text-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
@@ -161,15 +160,15 @@ export function AssistantModal({
</div>
<button
onClick={() => router.push("/assistants/new")}
className="h-10 cursor-pointer px-6 py-3 bg-black rounded-md border border-black justify-center items-center gap-2.5 inline-flex"
className="h-10 cursor-pointer px-6 py-3 bg-background-800 hover:bg-black rounded-md border border-black justify-center items-center gap-2.5 inline-flex"
>
<div className="text-[#fffcf4] text-lg font-normal leading-normal">
<div className="text-text-50 text-lg font-normal leading-normal">
Create
</div>
</button>
</div>
<div className="px-2 flex py-4 items-center gap-x-2 flex-wrap">
<FilterIcon size={16} />
<FilterIcon className="text-text-800" size={16} />
<AssistantBadgeSelector
text="Pinned"
selected={assistantFilters[AssistantFilter.Pinned]}
@@ -198,15 +197,15 @@ export function AssistantModal({
}
/>
</div>
<div className="w-full border-t border-neutral-200" />
<div className="w-full border-t border-background-200" />
</div>
<div className="flex-grow overflow-y-auto">
<h2 className="text-2xl font-semibold text-gray-800 mb-2 px-4 py-2">
<h2 className="text-2xl font-semibold text-text-800 mb-2 px-4 py-2">
Featured Assistants
</h2>
<div className="w-full px-2 pb-2 grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-6">
<div className="w-full px-2 pb-10 grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-6">
{featuredAssistants.length > 0 ? (
featuredAssistants.map((assistant, index) => (
<div key={index}>
@@ -220,7 +219,7 @@ export function AssistantModal({
</div>
))
) : (
<div className="col-span-2 text-center text-gray-500">
<div className="col-span-2 text-center text-text-500">
No featured assistants match filters
</div>
)}
@@ -228,7 +227,7 @@ export function AssistantModal({
{allAssistants && allAssistants.length > 0 && (
<>
<h2 className="text-2xl font-semibold text-gray-800 mt-4 mb-2 px-4 py-2">
<h2 className="text-2xl font-semibold text-text-800 mt-4 mb-2 px-4 py-2">
All Assistants
</h2>
@@ -253,8 +252,8 @@ export function AssistantModal({
)}
</div>
</div>
</div>
</div>
</DialogContent>
</Dialog>
);
}
export default AssistantModal;

View File

@@ -195,7 +195,7 @@ export function UserDropdown({
) : hideUserDropdown ? (
<DropdownOption
onClick={() => router.push("/auth/login")}
icon={<UserIcon className="h-5w-5 my-auto " />}
icon={<UserIcon className="h-5 w-5 my-auto " />}
label="Log In"
/>
) : (
@@ -208,14 +208,14 @@ export function UserDropdown({
item.svg_logo ? (
<div
className="
h-4
w-4
my-auto
overflow-hidden
flex
items-center
justify-center
"
h-4
w-4
my-auto
overflow-hidden
flex
items-center
justify-center
"
aria-label={item.title}
>
<svg

View File

@@ -62,7 +62,7 @@ export function Popover({
<RadixPopover.Portal>
<RadixPopover.Content
className={`
PopoverContent z-[100]
PopoverContent z-50
${contentClassName}
${matchWidth ? " PopoverContentMatchWidth" : ""}
`}

View File

@@ -647,11 +647,11 @@ export const useUserGroups = (): {
const MODEL_DISPLAY_NAMES: { [key: string]: string } = {
// OpenAI models
"o1-2025-12-17": "o1 (December 2025)",
"o3-mini": "o3 Mini",
"o1-mini": "o1 Mini",
"o1-preview": "o1 Preview",
o1: "o1",
"o1-2025-12-17": "O1 (December 2025)",
"o3-mini": "O3 Mini",
"o1-mini": "O1 Mini",
"o1-preview": "O1 Preview",
o1: "O1",
"gpt-4": "GPT 4",
"gpt-4o": "GPT 4o",
"gpt-4o-2024-08-06": "GPT 4o (Structured Outputs)",
@@ -753,7 +753,14 @@ export function getDisplayNameForModel(modelName: string): string {
}
export const defaultModelsByProvider: { [name: string]: string[] } = {
openai: ["gpt-4", "gpt-4o", "gpt-4o-mini", "o3-mini", "o1-mini", "o1"],
openai: [
"gpt-4",
"gpt-4o",
"gpt-4o-mini",
"o3-mini",
"o1-mini",
"o1-preview",
],
bedrock: [
"meta.llama3-1-70b-instruct-v1:0",
"meta.llama3-1-8b-instruct-v1:0",