Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 45 additions & 9 deletions electron/embedding-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,55 @@ export type EmbeddingResult =
*/
export class EmbeddingService {
private openaiClient: OpenAI | null = null
private pineconeClient: Pinecone | null = null
private pineconeClient: Pinecone | null = null // Data client (from connection profile)
private inferenceClient: Pinecone | null = null // Inference client (for local mode, uses Settings API key)
private isLocalMode: boolean = false

/**
* Set the Pinecone client instance (passed from PineconeService)
* This is the data client used for cloud connections
*/
setPineconeClient(client: Pinecone): void {
this.pineconeClient = client
}

/**
* Set local mode and configure inference client if API key available
* In local mode, Pinecone Inference requires a separate API key from Settings
*/
setLocalMode(isLocal: boolean, inferenceApiKey?: string): void {
this.isLocalMode = isLocal
if (isLocal && inferenceApiKey) {
this.inferenceClient = new Pinecone({ apiKey: inferenceApiKey })
} else {
this.inferenceClient = null
}
}

/**
* Get the appropriate client for Pinecone Inference API calls
* - Cloud mode: use the profile's data client (same API key for data + inference)
* - Local mode: use separate inference client with Settings API key
*/
private getPineconeInferenceClient(): Pinecone {
if (this.isLocalMode) {
if (!this.inferenceClient) {
throw new EmbeddingCredentialsError(
'Pinecone',
'PINECONE_API_KEY',
'Pinecone API key required for embeddings in local mode. Configure it in Settings → API Keys.'
)
}
return this.inferenceClient
}

// Cloud mode: use the profile's client
if (!this.pineconeClient) {
throw new Error('Pinecone client not initialized. Please connect first.')
}
return this.pineconeClient
}

/**
* Generate embeddings for an array of texts.
* Returns EmbeddingResult which can be either dense or sparse based on the model.
Expand Down Expand Up @@ -109,9 +149,7 @@ export class EmbeddingService {
texts: string[],
config: EmbeddingConfig
): Promise<EmbeddingResult> {
if (!this.pineconeClient) {
throw new Error('Pinecone client not initialized. Please connect first.')
}
const client = this.getPineconeInferenceClient()

const inputType = config.inputType || 'passage'

Expand All @@ -125,7 +163,7 @@ export class EmbeddingService {
params.dimension = config.dimensions
}

const response = await this.pineconeClient.inference.embed(
const response = await client.inference.embed(
config.modelName,
texts,
params as Record<string, string>
Expand All @@ -150,13 +188,11 @@ export class EmbeddingService {
texts: string[],
config: EmbeddingConfig
): Promise<EmbeddingResult> {
if (!this.pineconeClient) {
throw new Error('Pinecone client not initialized. Please connect first.')
}
const client = this.getPineconeInferenceClient()

const inputType = config.inputType || 'passage'

const response = await this.pineconeClient.inference.embed(
const response = await client.inference.embed(
config.modelName,
texts,
{ inputType, truncate: 'END' } as Record<string, string>
Expand Down
54 changes: 47 additions & 7 deletions electron/pinecone-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
} from './types'
import { EmbeddingService, SparseVector, EmbeddingResult } from './embedding-service'
import { withRetry } from './retry-utils'
import { settingsStore } from './settings-store'

/**
* Main Pinecone service class
Expand All @@ -44,25 +45,43 @@ class PineconeService {
return this.profile
}

/**
* Check if current connection is in local mode
*/
isLocalMode(): boolean {
return !!this.profile?.controllerHostUrl
}

/**
* Connect to Pinecone with the given profile
*/
async connect(profile: ConnectionProfile): Promise<void> {
try {
this.client = new Pinecone({
apiKey: profile.apiKey,
...(profile.controllerHostUrl && { controllerHostUrl: profile.controllerHostUrl }),
})

// Test connection by listing indexes
await this.client.listIndexes()
// Store profile first (needed by listIndexes for local instance handling)
this.profile = profile
this.indexCache.clear()

// Initialize embedding service and pass Pinecone client
this.embeddingService = new EmbeddingService()
this.embeddingService.setPineconeClient(this.client)

// Store profile on successful connection
this.profile = profile
this.indexCache.clear()
// Configure local mode for embedding service
// In local mode, Pinecone Inference needs a separate API key from Settings
const isLocalMode = !!profile.controllerHostUrl
if (isLocalMode) {
const apiKeys = settingsStore.getApiKeys()
this.embeddingService.setLocalMode(true, apiKeys.PINECONE_API_KEY)
} else {
this.embeddingService.setLocalMode(false)
}

// Test connection and populate index cache
await this.listIndexes()
} catch (error) {
this.client = null
this.embeddingService = null
Expand Down Expand Up @@ -91,6 +110,7 @@ class PineconeService {

/**
* Get or create an index reference
* For Pinecone Local, explicitly passes the host URL with http:// prefix
*/
private getIndex(indexName: string): Index<RecordMetadata> {
if (!this.client) {
Expand All @@ -99,7 +119,22 @@ class PineconeService {

let index = this.indexCache.get(indexName)
if (!index) {
index = this.client.index(indexName)
// For Pinecone Local, we need to explicitly provide the host URL
if (this.profile?.controllerHostUrl) {
const indexInfo = this.indexInfoCache.get(indexName)
if (indexInfo?.host) {
// Pinecone Local returns hosts without protocol, need to add http://
const hostUrl = indexInfo.host.startsWith('http')
? indexInfo.host
: `http://${indexInfo.host}`
index = this.client.index(indexName, hostUrl)
} else {
// Fallback if index info not cached yet
index = this.client.index(indexName)
}
} else {
index = this.client.index(indexName)
}
this.indexCache.set(indexName, index)
}
return index
Expand Down Expand Up @@ -662,8 +697,13 @@ class PineconeService {
fields.push('*') // Request all fields
}

// Reranking is not supported in local mode
if (this.isLocalMode()) {
throw new Error('Reranking is not available for Pinecone Local. Disable reranking to search.')
}

try {
// Call searchRecords API
// Cloud mode: use searchRecords API (server-side reranking)
// Type for searchRecords response
interface SearchRecordsResponse {
result?: {
Expand Down
1 change: 1 addition & 0 deletions electron/settings-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { getEncryptionKey } from './secure-key-manager'

export interface ApiKeys {
OPENAI_API_KEY?: string
PINECONE_API_KEY?: string // For Pinecone Inference (embeddings/reranking) - required for local mode
}

export type Theme = 'light' | 'dark' | 'system'
Expand Down
3 changes: 3 additions & 0 deletions electron/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ export interface ConnectionProfile {
// Pinecone authentication
apiKey: string // Pinecone API key

// Optional: Custom host for Pinecone Local (e.g., 'http://localhost:5080')
controllerHostUrl?: string

createdAt: number // Timestamp
lastUsed?: number // Timestamp

Expand Down
Loading