Compare commits

...

4 Commits

13 changed files with 129 additions and 37 deletions

View File

@@ -72,9 +72,9 @@ describe("ActionSuggestions", () => {
it("should render both GitHub buttons when GitHub token is set and repository is selected", async () => {
const getConversationSpy = vi.spyOn(OpenHands, "getConversation");
// @ts-expect-error - only required for testing
getConversationSpy.mockResolvedValue({
selected_repository: "test-repo",
// @ts-expect-error - only required for testing
repository: { full_name: "test-repo" },
});
renderActionSuggestions();

View File

@@ -41,7 +41,7 @@ describe("ConversationPanel", () => {
{
conversation_id: "1",
title: "Conversation 1",
selected_repository: null,
repository: null,
last_updated_at: "2021-10-01T12:00:00Z",
created_at: "2021-10-01T12:00:00Z",
status: "STOPPED" as const,
@@ -51,7 +51,7 @@ describe("ConversationPanel", () => {
{
conversation_id: "2",
title: "Conversation 2",
selected_repository: null,
repository: null,
last_updated_at: "2021-10-02T12:00:00Z",
created_at: "2021-10-02T12:00:00Z",
status: "STOPPED" as const,
@@ -61,7 +61,7 @@ describe("ConversationPanel", () => {
{
conversation_id: "3",
title: "Conversation 3",
selected_repository: null,
repository: null,
last_updated_at: "2021-10-03T12:00:00Z",
created_at: "2021-10-03T12:00:00Z",
status: "STOPPED" as const,
@@ -145,7 +145,7 @@ describe("ConversationPanel", () => {
{
conversation_id: "1",
title: "Conversation 1",
selected_repository: null,
repository: null,
last_updated_at: "2021-10-01T12:00:00Z",
created_at: "2021-10-01T12:00:00Z",
status: "STOPPED" as const,
@@ -155,7 +155,7 @@ describe("ConversationPanel", () => {
{
conversation_id: "2",
title: "Conversation 2",
selected_repository: null,
repository: null,
last_updated_at: "2021-10-02T12:00:00Z",
created_at: "2021-10-02T12:00:00Z",
status: "STOPPED" as const,
@@ -165,7 +165,7 @@ describe("ConversationPanel", () => {
{
conversation_id: "3",
title: "Conversation 3",
selected_repository: null,
repository: null,
last_updated_at: "2021-10-03T12:00:00Z",
created_at: "2021-10-03T12:00:00Z",
status: "STOPPED" as const,

View File

@@ -1,4 +1,5 @@
import { ProjectStatus } from "#/components/features/conversation-panel/conversation-state-indicator";
import { Provider } from "#/types/settings";
export interface ErrorResponse {
error: string;
@@ -72,10 +73,17 @@ export interface AuthenticateResponse {
export type ConversationTrigger = "resolver" | "gui" | "suggested_task";
export interface RepositoryInfo {
full_name: string;
id: number | null;
git_provider: Provider | null;
is_public: boolean | null;
}
export interface Conversation {
conversation_id: string;
title: string;
selected_repository: string | null;
repository: RepositoryInfo | null;
last_updated_at: string;
created_at: string;
status: ProjectStatus;

View File

@@ -38,7 +38,7 @@ export function ActionSuggestions({
return (
<div className="flex flex-col gap-2 mb-2">
{providersAreSet && conversation?.selected_repository && (
{providersAreSet && conversation?.repository && (
<div className="flex flex-row gap-2 justify-center w-full">
{!hasPullRequest ? (
<>

View File

@@ -29,7 +29,7 @@ export function Controls({ setSecurityOpen, showSecurityLock }: ControlsProps) {
showOptions
title={conversation?.title ?? ""}
lastUpdatedAt={conversation?.created_at ?? ""}
selectedRepository={conversation?.selected_repository ?? null}
selectedRepository={conversation?.repository?.full_name ?? null}
status={conversation?.status}
conversationId={conversation?.conversation_id}
/>

View File

@@ -88,7 +88,7 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) {
isActive={isActive}
onDelete={() => handleDeleteProject(project.conversation_id)}
title={project.title}
selectedRepository={project.selected_repository}
selectedRepository={project.repository?.full_name ?? null}
lastUpdatedAt={project.last_updated_at}
createdAt={project.created_at}
status={project.status}

View File

@@ -236,7 +236,7 @@ export function WsClientProvider({
conversationId,
]);
const clonedRepositoryDirectory =
cachedConversaton?.selected_repository?.split("/").pop();
cachedConversaton?.repository?.full_name?.split("/").pop();
let fileToInvalidate = event.args.path.replace("/workspace/", "");
if (clonedRepositoryDirectory) {

View File

@@ -1,5 +1,7 @@
// this file generate by script, don't modify it manually!!!
export enum I18nKey {
HOME$LAUNCH_FROM_SCRATCH = "HOME$LAUNCH_FROM_SCRATCH",
HOME$READ_THIS = "HOME$READ_THIS",
AUTH$LOGGING_BACK_IN = "AUTH$LOGGING_BACK_IN",
SECURITY$LOW_RISK = "SECURITY$LOW_RISK",
SECURITY$MEDIUM_RISK = "SECURITY$MEDIUM_RISK",

View File

@@ -51,7 +51,7 @@ const conversations: Conversation[] = [
{
conversation_id: "1",
title: "My New Project",
selected_repository: null,
repository: null,
last_updated_at: new Date().toISOString(),
created_at: new Date().toISOString(),
status: "RUNNING",
@@ -61,7 +61,12 @@ const conversations: Conversation[] = [
{
conversation_id: "2",
title: "Repo Testing",
selected_repository: "octocat/hello-world",
repository: {
id: 2,
full_name: "octocat/hello-world",
git_provider: "github",
is_public: true,
},
// 2 days ago
last_updated_at: new Date(
Date.now() - 2 * 24 * 60 * 60 * 1000,
@@ -74,7 +79,12 @@ const conversations: Conversation[] = [
{
conversation_id: "3",
title: "Another Project",
selected_repository: "octocat/earth",
repository: {
id: 3,
full_name: "octocat/earth",
git_provider: "github",
is_public: true,
},
// 5 days ago
last_updated_at: new Date(
Date.now() - 5 * 24 * 60 * 60 * 1000,
@@ -270,7 +280,7 @@ export const handlers = [
const conversation: Conversation = {
conversation_id: (Math.random() * 100).toString(),
title: "New Conversation",
selected_repository: null,
repository: null,
last_updated_at: new Date().toISOString(),
created_at: new Date().toISOString(),
status: "RUNNING",

View File

@@ -1,10 +1,23 @@
from dataclasses import dataclass, field
from datetime import datetime, timezone
from openhands.integrations.service_types import ProviderType
from openhands.storage.data_models.conversation_metadata import ConversationTrigger
from openhands.storage.data_models.conversation_status import ConversationStatus
@dataclass
class RepositoryInfo:
"""
Information about a repository associated with a conversation
"""
full_name: str
id: int | None = None
git_provider: ProviderType | None = None
is_public: bool | None = None
@dataclass
class ConversationInfo:
"""
@@ -16,9 +29,9 @@ class ConversationInfo:
title: str
last_updated_at: datetime | None = None
status: ConversationStatus = ConversationStatus.STOPPED
selected_repository: str | None = None
trigger: ConversationTrigger | None = None
num_connections: int = 0
url: str | None = None
session_api_key: str | None = None
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
repository: RepositoryInfo | None = None

View File

@@ -1,4 +1,3 @@
import asyncio
import uuid
from datetime import datetime, timezone
@@ -18,7 +17,10 @@ from openhands.integrations.service_types import (
)
from openhands.runtime import get_runtime_cls
from openhands.server.data_models.agent_loop_info import AgentLoopInfo
from openhands.server.data_models.conversation_info import ConversationInfo
from openhands.server.data_models.conversation_info import (
ConversationInfo,
RepositoryInfo,
)
from openhands.server.data_models.conversation_info_result_set import (
ConversationInfoResultSet,
)
@@ -106,8 +108,10 @@ async def new_conversation(
if auth_type == AuthType.BEARER:
conversation_trigger = ConversationTrigger.REMOTE_API_KEY
if conversation_trigger == ConversationTrigger.REMOTE_API_KEY and not initial_user_msg:
if (
conversation_trigger == ConversationTrigger.REMOTE_API_KEY
and not initial_user_msg
):
return JSONResponse(
content={
'status': 'error',
@@ -118,12 +122,25 @@ async def new_conversation(
)
try:
repo_details = None
if repository:
provider_handler = ProviderHandler(provider_tokens)
# Check against git_provider, otherwise check all provider apis
await provider_handler.verify_repo_provider(repository, git_provider)
repo_details = await provider_handler.verify_repo_provider(
repository, git_provider
)
conversation_id = data.conversation_id
# Extract repository details if available
repository_id = None
is_public = None
actual_git_provider = git_provider
if repo_details:
repository_id = repo_details.id
is_public = repo_details.is_public
actual_git_provider = repo_details.git_provider
await create_new_conversation(
user_id=user_id,
git_provider_tokens=provider_tokens,
@@ -135,8 +152,10 @@ async def new_conversation(
replay_json=replay_json,
conversation_trigger=conversation_trigger,
conversation_instructions=conversation_instructions,
git_provider=git_provider,
git_provider=actual_git_provider,
conversation_id=conversation_id,
repository_id=repository_id,
is_public=is_public,
)
return InitSessionResponse(
@@ -196,19 +215,27 @@ async def search_conversations(
conversation_ids = set(
conversation.conversation_id for conversation in filtered_results
)
connection_ids_to_conversation_ids = await conversation_manager.get_connections(filter_to_sids=conversation_ids)
agent_loop_info = await conversation_manager.get_agent_loop_info(filter_to_sids=conversation_ids)
agent_loop_info_by_conversation_id = {info.conversation_id: info for info in agent_loop_info}
connection_ids_to_conversation_ids = await conversation_manager.get_connections(
filter_to_sids=conversation_ids
)
agent_loop_info = await conversation_manager.get_agent_loop_info(
filter_to_sids=conversation_ids
)
agent_loop_info_by_conversation_id = {
info.conversation_id: info for info in agent_loop_info
}
result = ConversationInfoResultSet(
results=await wait_all(
_get_conversation_info(
conversation=conversation,
num_connections=sum(
1 for conversation_id in connection_ids_to_conversation_ids.values()
1
for conversation_id in connection_ids_to_conversation_ids.values()
if conversation_id == conversation.conversation_id
),
agent_loop_info=agent_loop_info_by_conversation_id.get(conversation.conversation_id),
agent_loop_info=agent_loop_info_by_conversation_id.get(
conversation.conversation_id
),
)
for conversation in filtered_results
),
@@ -224,10 +251,16 @@ async def get_conversation(
) -> ConversationInfo | None:
try:
metadata = await conversation_store.get_metadata(conversation_id)
num_connections = len(await conversation_manager.get_connections(filter_to_sids={conversation_id}))
agent_loop_infos = await conversation_manager.get_agent_loop_info(filter_to_sids={conversation_id})
num_connections = len(
await conversation_manager.get_connections(filter_to_sids={conversation_id})
)
agent_loop_infos = await conversation_manager.get_agent_loop_info(
filter_to_sids={conversation_id}
)
agent_loop_info = agent_loop_infos[0] if agent_loop_infos else None
conversation_info = await _get_conversation_info(metadata, num_connections, agent_loop_info)
conversation_info = await _get_conversation_info(
metadata, num_connections, agent_loop_info
)
return conversation_info
except FileNotFoundError:
return None
@@ -261,19 +294,34 @@ async def _get_conversation_info(
title = conversation.title
if not title:
title = get_default_conversation_title(conversation.conversation_id)
# Create repository info if a repository is selected
repository = None
if conversation.selected_repository:
repository = RepositoryInfo(
full_name=conversation.selected_repository,
id=conversation.repository_id,
git_provider=conversation.git_provider,
is_public=conversation.is_public,
)
return ConversationInfo(
trigger=conversation.trigger,
conversation_id=conversation.conversation_id,
title=title,
last_updated_at=conversation.last_updated_at,
created_at=conversation.created_at,
selected_repository=conversation.selected_repository,
status=(
agent_loop_info.status if agent_loop_info else ConversationStatus.STOPPED
agent_loop_info.status
if agent_loop_info
else ConversationStatus.STOPPED
),
num_connections=num_connections,
url=agent_loop_info.url if agent_loop_info else None,
session_api_key=agent_loop_info.session_api_key if agent_loop_info else None,
session_api_key=agent_loop_info.session_api_key
if agent_loop_info
else None,
repository=repository,
)
except Exception as e:
logger.error(

View File

@@ -38,6 +38,8 @@ async def create_new_conversation(
attach_convo_id: bool = False,
git_provider: ProviderType | None = None,
conversation_id: str | None = None,
repository_id: int | None = None,
is_public: bool | None = None,
) -> AgentLoopInfo:
logger.info(
'Creating conversation',
@@ -103,6 +105,9 @@ async def create_new_conversation(
user_id=user_id,
selected_repository=selected_repository,
selected_branch=selected_branch,
git_provider=git_provider,
repository_id=repository_id,
is_public=is_public,
)
)

View File

@@ -2,6 +2,8 @@ from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum
from openhands.integrations.service_types import ProviderType
class ConversationTrigger(Enum):
RESOLVER = 'resolver'
@@ -20,10 +22,14 @@ class ConversationMetadata:
title: str | None = None
last_updated_at: datetime | None = None
trigger: ConversationTrigger | None = None
pr_number: list[int] = field(default_factory=list)
pr_number: list[int] = field(default_factory=list)
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
# Cost and token metrics
accumulated_cost: float = 0.0
prompt_tokens: int = 0
completion_tokens: int = 0
total_tokens: int = 0
# Additional repository fields
repository_id: int | None = None
git_provider: ProviderType | None = None
is_public: bool | None = None