mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-25 00:16:29 +02:00
Compare commits
2 commits
bdf270b7a1
...
d42fb26bcc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d42fb26bcc | ||
|
|
caf00fae0c |
25 changed files with 344 additions and 47 deletions
|
|
@ -59,14 +59,14 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
|
||||||
const [modelsCatalog, setModelsCatalog] = useState<Record<string, LlmModelOption[]>>({})
|
const [modelsCatalog, setModelsCatalog] = useState<Record<string, LlmModelOption[]>>({})
|
||||||
const [modelsLoading, setModelsLoading] = useState(false)
|
const [modelsLoading, setModelsLoading] = useState(false)
|
||||||
const [modelsError, setModelsError] = useState<string | null>(null)
|
const [modelsError, setModelsError] = useState<string | null>(null)
|
||||||
const [providerConfigs, setProviderConfigs] = useState<Record<LlmProviderFlavor, { apiKey: string; baseURL: string; model: string; knowledgeGraphModel: string }>>({
|
const [providerConfigs, setProviderConfigs] = useState<Record<LlmProviderFlavor, { apiKey: string; baseURL: string; model: string; knowledgeGraphModel: string; meetingNotesModel: string; trackBlockModel: string }>>({
|
||||||
openai: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "" },
|
openai: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||||
anthropic: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "" },
|
anthropic: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||||
google: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "" },
|
google: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||||
openrouter: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "" },
|
openrouter: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||||
aigateway: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "" },
|
aigateway: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||||
ollama: { apiKey: "", baseURL: "http://localhost:11434", model: "", knowledgeGraphModel: "" },
|
ollama: { apiKey: "", baseURL: "http://localhost:11434", model: "", knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||||
"openai-compatible": { apiKey: "", baseURL: "http://localhost:1234/v1", model: "", knowledgeGraphModel: "" },
|
"openai-compatible": { apiKey: "", baseURL: "http://localhost:1234/v1", model: "", knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||||
})
|
})
|
||||||
const [testState, setTestState] = useState<{ status: "idle" | "testing" | "success" | "error"; error?: string }>({
|
const [testState, setTestState] = useState<{ status: "idle" | "testing" | "success" | "error"; error?: string }>({
|
||||||
status: "idle",
|
status: "idle",
|
||||||
|
|
@ -109,7 +109,7 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
|
||||||
const [googleCalendarConnecting, setGoogleCalendarConnecting] = useState(false)
|
const [googleCalendarConnecting, setGoogleCalendarConnecting] = useState(false)
|
||||||
|
|
||||||
const updateProviderConfig = useCallback(
|
const updateProviderConfig = useCallback(
|
||||||
(provider: LlmProviderFlavor, updates: Partial<{ apiKey: string; baseURL: string; model: string; knowledgeGraphModel: string }>) => {
|
(provider: LlmProviderFlavor, updates: Partial<{ apiKey: string; baseURL: string; model: string; knowledgeGraphModel: string; meetingNotesModel: string; trackBlockModel: string }>) => {
|
||||||
setProviderConfigs(prev => ({
|
setProviderConfigs(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
[provider]: { ...prev[provider], ...updates },
|
[provider]: { ...prev[provider], ...updates },
|
||||||
|
|
@ -458,6 +458,8 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
|
||||||
const baseURL = activeConfig.baseURL.trim() || undefined
|
const baseURL = activeConfig.baseURL.trim() || undefined
|
||||||
const model = activeConfig.model.trim()
|
const model = activeConfig.model.trim()
|
||||||
const knowledgeGraphModel = activeConfig.knowledgeGraphModel.trim() || undefined
|
const knowledgeGraphModel = activeConfig.knowledgeGraphModel.trim() || undefined
|
||||||
|
const meetingNotesModel = activeConfig.meetingNotesModel.trim() || undefined
|
||||||
|
const trackBlockModel = activeConfig.trackBlockModel.trim() || undefined
|
||||||
const providerConfig = {
|
const providerConfig = {
|
||||||
provider: {
|
provider: {
|
||||||
flavor: llmProvider,
|
flavor: llmProvider,
|
||||||
|
|
@ -466,6 +468,8 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
|
||||||
},
|
},
|
||||||
model,
|
model,
|
||||||
knowledgeGraphModel,
|
knowledgeGraphModel,
|
||||||
|
meetingNotesModel,
|
||||||
|
trackBlockModel,
|
||||||
}
|
}
|
||||||
const result = await window.ipc.invoke("models:test", providerConfig)
|
const result = await window.ipc.invoke("models:test", providerConfig)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
|
@ -1157,6 +1161,72 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Meeting notes model</span>
|
||||||
|
{modelsLoading ? (
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<Loader2 className="size-4 animate-spin" />
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
) : showModelInput ? (
|
||||||
|
<Input
|
||||||
|
value={activeConfig.meetingNotesModel}
|
||||||
|
onChange={(e) => updateProviderConfig(llmProvider, { meetingNotesModel: e.target.value })}
|
||||||
|
placeholder={activeConfig.model || "Enter model"}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Select
|
||||||
|
value={activeConfig.meetingNotesModel || "__same__"}
|
||||||
|
onValueChange={(value) => updateProviderConfig(llmProvider, { meetingNotesModel: value === "__same__" ? "" : value })}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a model" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="__same__">Same as assistant</SelectItem>
|
||||||
|
{modelsForProvider.map((model) => (
|
||||||
|
<SelectItem key={model.id} value={model.id}>
|
||||||
|
{model.name || model.id}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Track block model</span>
|
||||||
|
{modelsLoading ? (
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<Loader2 className="size-4 animate-spin" />
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
) : showModelInput ? (
|
||||||
|
<Input
|
||||||
|
value={activeConfig.trackBlockModel}
|
||||||
|
onChange={(e) => updateProviderConfig(llmProvider, { trackBlockModel: e.target.value })}
|
||||||
|
placeholder={activeConfig.model || "Enter model"}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Select
|
||||||
|
value={activeConfig.trackBlockModel || "__same__"}
|
||||||
|
onValueChange={(value) => updateProviderConfig(llmProvider, { trackBlockModel: value === "__same__" ? "" : value })}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a model" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="__same__">Same as assistant</SelectItem>
|
||||||
|
{modelsForProvider.map((model) => (
|
||||||
|
<SelectItem key={model.id} value={model.id}>
|
||||||
|
{model.name || model.id}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showApiKey && (
|
{showApiKey && (
|
||||||
|
|
|
||||||
|
|
@ -221,6 +221,76 @@ export function LlmSetupStep({ state }: LlmSetupStepProps) {
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2 min-w-0">
|
||||||
|
<label className="text-xs font-medium text-muted-foreground">
|
||||||
|
Meeting Notes Model
|
||||||
|
</label>
|
||||||
|
{modelsLoading ? (
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<Loader2 className="size-4 animate-spin" />
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
) : showModelInput ? (
|
||||||
|
<Input
|
||||||
|
value={activeConfig.meetingNotesModel}
|
||||||
|
onChange={(e) => updateProviderConfig(llmProvider, { meetingNotesModel: e.target.value })}
|
||||||
|
placeholder={activeConfig.model || "Enter model"}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Select
|
||||||
|
value={activeConfig.meetingNotesModel || "__same__"}
|
||||||
|
onValueChange={(value) => updateProviderConfig(llmProvider, { meetingNotesModel: value === "__same__" ? "" : value })}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full truncate">
|
||||||
|
<SelectValue placeholder="Select a model" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="__same__">Same as assistant</SelectItem>
|
||||||
|
{modelsForProvider.map((model) => (
|
||||||
|
<SelectItem key={model.id} value={model.id}>
|
||||||
|
{model.name || model.id}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2 min-w-0">
|
||||||
|
<label className="text-xs font-medium text-muted-foreground">
|
||||||
|
Track Block Model
|
||||||
|
</label>
|
||||||
|
{modelsLoading ? (
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<Loader2 className="size-4 animate-spin" />
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
) : showModelInput ? (
|
||||||
|
<Input
|
||||||
|
value={activeConfig.trackBlockModel}
|
||||||
|
onChange={(e) => updateProviderConfig(llmProvider, { trackBlockModel: e.target.value })}
|
||||||
|
placeholder={activeConfig.model || "Enter model"}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Select
|
||||||
|
value={activeConfig.trackBlockModel || "__same__"}
|
||||||
|
onValueChange={(value) => updateProviderConfig(llmProvider, { trackBlockModel: value === "__same__" ? "" : value })}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full truncate">
|
||||||
|
<SelectValue placeholder="Select a model" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="__same__">Same as assistant</SelectItem>
|
||||||
|
{modelsForProvider.map((model) => (
|
||||||
|
<SelectItem key={model.id} value={model.id}>
|
||||||
|
{model.name || model.id}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showApiKey && (
|
{showApiKey && (
|
||||||
|
|
|
||||||
|
|
@ -29,14 +29,14 @@ export function useOnboardingState(open: boolean, onComplete: () => void) {
|
||||||
const [modelsCatalog, setModelsCatalog] = useState<Record<string, LlmModelOption[]>>({})
|
const [modelsCatalog, setModelsCatalog] = useState<Record<string, LlmModelOption[]>>({})
|
||||||
const [modelsLoading, setModelsLoading] = useState(false)
|
const [modelsLoading, setModelsLoading] = useState(false)
|
||||||
const [modelsError, setModelsError] = useState<string | null>(null)
|
const [modelsError, setModelsError] = useState<string | null>(null)
|
||||||
const [providerConfigs, setProviderConfigs] = useState<Record<LlmProviderFlavor, { apiKey: string; baseURL: string; model: string; knowledgeGraphModel: string }>>({
|
const [providerConfigs, setProviderConfigs] = useState<Record<LlmProviderFlavor, { apiKey: string; baseURL: string; model: string; knowledgeGraphModel: string; meetingNotesModel: string; trackBlockModel: string }>>({
|
||||||
openai: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "" },
|
openai: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||||
anthropic: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "" },
|
anthropic: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||||
google: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "" },
|
google: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||||
openrouter: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "" },
|
openrouter: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||||
aigateway: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "" },
|
aigateway: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||||
ollama: { apiKey: "", baseURL: "http://localhost:11434", model: "", knowledgeGraphModel: "" },
|
ollama: { apiKey: "", baseURL: "http://localhost:11434", model: "", knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||||
"openai-compatible": { apiKey: "", baseURL: "http://localhost:1234/v1", model: "", knowledgeGraphModel: "" },
|
"openai-compatible": { apiKey: "", baseURL: "http://localhost:1234/v1", model: "", knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||||
})
|
})
|
||||||
const [testState, setTestState] = useState<{ status: "idle" | "testing" | "success" | "error"; error?: string }>({
|
const [testState, setTestState] = useState<{ status: "idle" | "testing" | "success" | "error"; error?: string }>({
|
||||||
status: "idle",
|
status: "idle",
|
||||||
|
|
@ -81,7 +81,7 @@ export function useOnboardingState(open: boolean, onComplete: () => void) {
|
||||||
const [googleCalendarConnecting, setGoogleCalendarConnecting] = useState(false)
|
const [googleCalendarConnecting, setGoogleCalendarConnecting] = useState(false)
|
||||||
|
|
||||||
const updateProviderConfig = useCallback(
|
const updateProviderConfig = useCallback(
|
||||||
(provider: LlmProviderFlavor, updates: Partial<{ apiKey: string; baseURL: string; model: string; knowledgeGraphModel: string }>) => {
|
(provider: LlmProviderFlavor, updates: Partial<{ apiKey: string; baseURL: string; model: string; knowledgeGraphModel: string; meetingNotesModel: string; trackBlockModel: string }>) => {
|
||||||
setProviderConfigs(prev => ({
|
setProviderConfigs(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
[provider]: { ...prev[provider], ...updates },
|
[provider]: { ...prev[provider], ...updates },
|
||||||
|
|
@ -435,6 +435,8 @@ export function useOnboardingState(open: boolean, onComplete: () => void) {
|
||||||
const baseURL = activeConfig.baseURL.trim() || undefined
|
const baseURL = activeConfig.baseURL.trim() || undefined
|
||||||
const model = activeConfig.model.trim()
|
const model = activeConfig.model.trim()
|
||||||
const knowledgeGraphModel = activeConfig.knowledgeGraphModel.trim() || undefined
|
const knowledgeGraphModel = activeConfig.knowledgeGraphModel.trim() || undefined
|
||||||
|
const meetingNotesModel = activeConfig.meetingNotesModel.trim() || undefined
|
||||||
|
const trackBlockModel = activeConfig.trackBlockModel.trim() || undefined
|
||||||
const providerConfig = {
|
const providerConfig = {
|
||||||
provider: {
|
provider: {
|
||||||
flavor: llmProvider,
|
flavor: llmProvider,
|
||||||
|
|
@ -443,6 +445,8 @@ export function useOnboardingState(open: boolean, onComplete: () => void) {
|
||||||
},
|
},
|
||||||
model,
|
model,
|
||||||
knowledgeGraphModel,
|
knowledgeGraphModel,
|
||||||
|
meetingNotesModel,
|
||||||
|
trackBlockModel,
|
||||||
}
|
}
|
||||||
const result = await window.ipc.invoke("models:test", providerConfig)
|
const result = await window.ipc.invoke("models:test", providerConfig)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
|
@ -459,7 +463,7 @@ export function useOnboardingState(open: boolean, onComplete: () => void) {
|
||||||
setTestState({ status: "error", error: "Connection test failed" })
|
setTestState({ status: "error", error: "Connection test failed" })
|
||||||
toast.error("Connection test failed")
|
toast.error("Connection test failed")
|
||||||
}
|
}
|
||||||
}, [activeConfig.apiKey, activeConfig.baseURL, activeConfig.model, activeConfig.knowledgeGraphModel, canTest, llmProvider, handleNext])
|
}, [activeConfig.apiKey, activeConfig.baseURL, activeConfig.model, activeConfig.knowledgeGraphModel, activeConfig.meetingNotesModel, activeConfig.trackBlockModel, canTest, llmProvider, handleNext])
|
||||||
|
|
||||||
// Check connection status for all providers
|
// Check connection status for all providers
|
||||||
const refreshAllStatuses = useCallback(async () => {
|
const refreshAllStatuses = useCallback(async () => {
|
||||||
|
|
|
||||||
|
|
@ -196,14 +196,14 @@ const defaultBaseURLs: Partial<Record<LlmProviderFlavor, string>> = {
|
||||||
function ModelSettings({ dialogOpen }: { dialogOpen: boolean }) {
|
function ModelSettings({ dialogOpen }: { dialogOpen: boolean }) {
|
||||||
const [provider, setProvider] = useState<LlmProviderFlavor>("openai")
|
const [provider, setProvider] = useState<LlmProviderFlavor>("openai")
|
||||||
const [defaultProvider, setDefaultProvider] = useState<LlmProviderFlavor | null>(null)
|
const [defaultProvider, setDefaultProvider] = useState<LlmProviderFlavor | null>(null)
|
||||||
const [providerConfigs, setProviderConfigs] = useState<Record<LlmProviderFlavor, { apiKey: string; baseURL: string; models: string[]; knowledgeGraphModel: string }>>({
|
const [providerConfigs, setProviderConfigs] = useState<Record<LlmProviderFlavor, { apiKey: string; baseURL: string; models: string[]; knowledgeGraphModel: string; meetingNotesModel: string; trackBlockModel: string }>>({
|
||||||
openai: { apiKey: "", baseURL: "", models: [""], knowledgeGraphModel: "" },
|
openai: { apiKey: "", baseURL: "", models: [""], knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||||
anthropic: { apiKey: "", baseURL: "", models: [""], knowledgeGraphModel: "" },
|
anthropic: { apiKey: "", baseURL: "", models: [""], knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||||
google: { apiKey: "", baseURL: "", models: [""], knowledgeGraphModel: "" },
|
google: { apiKey: "", baseURL: "", models: [""], knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||||
openrouter: { apiKey: "", baseURL: "", models: [""], knowledgeGraphModel: "" },
|
openrouter: { apiKey: "", baseURL: "", models: [""], knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||||
aigateway: { apiKey: "", baseURL: "", models: [""], knowledgeGraphModel: "" },
|
aigateway: { apiKey: "", baseURL: "", models: [""], knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||||
ollama: { apiKey: "", baseURL: "http://localhost:11434", models: [""], knowledgeGraphModel: "" },
|
ollama: { apiKey: "", baseURL: "http://localhost:11434", models: [""], knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||||
"openai-compatible": { apiKey: "", baseURL: "http://localhost:1234/v1", models: [""], knowledgeGraphModel: "" },
|
"openai-compatible": { apiKey: "", baseURL: "http://localhost:1234/v1", models: [""], knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||||
})
|
})
|
||||||
const [modelsCatalog, setModelsCatalog] = useState<Record<string, LlmModelOption[]>>({})
|
const [modelsCatalog, setModelsCatalog] = useState<Record<string, LlmModelOption[]>>({})
|
||||||
const [modelsLoading, setModelsLoading] = useState(false)
|
const [modelsLoading, setModelsLoading] = useState(false)
|
||||||
|
|
@ -229,7 +229,7 @@ function ModelSettings({ dialogOpen }: { dialogOpen: boolean }) {
|
||||||
(!requiresBaseURL || activeConfig.baseURL.trim().length > 0)
|
(!requiresBaseURL || activeConfig.baseURL.trim().length > 0)
|
||||||
|
|
||||||
const updateConfig = useCallback(
|
const updateConfig = useCallback(
|
||||||
(prov: LlmProviderFlavor, updates: Partial<{ apiKey: string; baseURL: string; models: string[]; knowledgeGraphModel: string }>) => {
|
(prov: LlmProviderFlavor, updates: Partial<{ apiKey: string; baseURL: string; models: string[]; knowledgeGraphModel: string; meetingNotesModel: string; trackBlockModel: string }>) => {
|
||||||
setProviderConfigs(prev => ({
|
setProviderConfigs(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
[prov]: { ...prev[prov], ...updates },
|
[prov]: { ...prev[prov], ...updates },
|
||||||
|
|
@ -302,6 +302,8 @@ function ModelSettings({ dialogOpen }: { dialogOpen: boolean }) {
|
||||||
baseURL: e.baseURL || (defaultBaseURLs[key as LlmProviderFlavor] || ""),
|
baseURL: e.baseURL || (defaultBaseURLs[key as LlmProviderFlavor] || ""),
|
||||||
models: savedModels,
|
models: savedModels,
|
||||||
knowledgeGraphModel: e.knowledgeGraphModel || "",
|
knowledgeGraphModel: e.knowledgeGraphModel || "",
|
||||||
|
meetingNotesModel: e.meetingNotesModel || "",
|
||||||
|
trackBlockModel: e.trackBlockModel || "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -318,6 +320,8 @@ function ModelSettings({ dialogOpen }: { dialogOpen: boolean }) {
|
||||||
baseURL: parsed.provider.baseURL || (defaultBaseURLs[flavor] || ""),
|
baseURL: parsed.provider.baseURL || (defaultBaseURLs[flavor] || ""),
|
||||||
models: activeModels.length > 0 ? activeModels : [""],
|
models: activeModels.length > 0 ? activeModels : [""],
|
||||||
knowledgeGraphModel: parsed.knowledgeGraphModel || "",
|
knowledgeGraphModel: parsed.knowledgeGraphModel || "",
|
||||||
|
meetingNotesModel: parsed.meetingNotesModel || "",
|
||||||
|
trackBlockModel: parsed.trackBlockModel || "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return next;
|
return next;
|
||||||
|
|
@ -391,6 +395,8 @@ function ModelSettings({ dialogOpen }: { dialogOpen: boolean }) {
|
||||||
model: allModels[0] || "",
|
model: allModels[0] || "",
|
||||||
models: allModels,
|
models: allModels,
|
||||||
knowledgeGraphModel: activeConfig.knowledgeGraphModel.trim() || undefined,
|
knowledgeGraphModel: activeConfig.knowledgeGraphModel.trim() || undefined,
|
||||||
|
meetingNotesModel: activeConfig.meetingNotesModel.trim() || undefined,
|
||||||
|
trackBlockModel: activeConfig.trackBlockModel.trim() || undefined,
|
||||||
}
|
}
|
||||||
const result = await window.ipc.invoke("models:test", providerConfig)
|
const result = await window.ipc.invoke("models:test", providerConfig)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
|
@ -423,6 +429,8 @@ function ModelSettings({ dialogOpen }: { dialogOpen: boolean }) {
|
||||||
model: allModels[0],
|
model: allModels[0],
|
||||||
models: allModels,
|
models: allModels,
|
||||||
knowledgeGraphModel: config.knowledgeGraphModel.trim() || undefined,
|
knowledgeGraphModel: config.knowledgeGraphModel.trim() || undefined,
|
||||||
|
meetingNotesModel: config.meetingNotesModel.trim() || undefined,
|
||||||
|
trackBlockModel: config.trackBlockModel.trim() || undefined,
|
||||||
})
|
})
|
||||||
setDefaultProvider(prov)
|
setDefaultProvider(prov)
|
||||||
window.dispatchEvent(new Event('models-config-changed'))
|
window.dispatchEvent(new Event('models-config-changed'))
|
||||||
|
|
@ -452,6 +460,8 @@ function ModelSettings({ dialogOpen }: { dialogOpen: boolean }) {
|
||||||
parsed.model = defModels[0] || ""
|
parsed.model = defModels[0] || ""
|
||||||
parsed.models = defModels
|
parsed.models = defModels
|
||||||
parsed.knowledgeGraphModel = defConfig.knowledgeGraphModel.trim() || undefined
|
parsed.knowledgeGraphModel = defConfig.knowledgeGraphModel.trim() || undefined
|
||||||
|
parsed.meetingNotesModel = defConfig.meetingNotesModel.trim() || undefined
|
||||||
|
parsed.trackBlockModel = defConfig.trackBlockModel.trim() || undefined
|
||||||
}
|
}
|
||||||
await window.ipc.invoke("workspace:writeFile", {
|
await window.ipc.invoke("workspace:writeFile", {
|
||||||
path: "config/models.json",
|
path: "config/models.json",
|
||||||
|
|
@ -459,7 +469,7 @@ function ModelSettings({ dialogOpen }: { dialogOpen: boolean }) {
|
||||||
})
|
})
|
||||||
setProviderConfigs(prev => ({
|
setProviderConfigs(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
[prov]: { apiKey: "", baseURL: defaultBaseURLs[prov] || "", models: [""], knowledgeGraphModel: "" },
|
[prov]: { apiKey: "", baseURL: defaultBaseURLs[prov] || "", models: [""], knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||||
}))
|
}))
|
||||||
setTestState({ status: "idle" })
|
setTestState({ status: "idle" })
|
||||||
window.dispatchEvent(new Event('models-config-changed'))
|
window.dispatchEvent(new Event('models-config-changed'))
|
||||||
|
|
@ -649,6 +659,74 @@ function ModelSettings({ dialogOpen }: { dialogOpen: boolean }) {
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Meeting notes model */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Meeting notes model</span>
|
||||||
|
{modelsLoading ? (
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<Loader2 className="size-4 animate-spin" />
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
) : showModelInput ? (
|
||||||
|
<Input
|
||||||
|
value={activeConfig.meetingNotesModel}
|
||||||
|
onChange={(e) => updateConfig(provider, { meetingNotesModel: e.target.value })}
|
||||||
|
placeholder={primaryModel || "Enter model"}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Select
|
||||||
|
value={activeConfig.meetingNotesModel || "__same__"}
|
||||||
|
onValueChange={(value) => updateConfig(provider, { meetingNotesModel: value === "__same__" ? "" : value })}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a model" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="__same__">Same as assistant</SelectItem>
|
||||||
|
{modelsForProvider.map((m) => (
|
||||||
|
<SelectItem key={m.id} value={m.id}>
|
||||||
|
{m.name || m.id}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Track block model */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Track block model</span>
|
||||||
|
{modelsLoading ? (
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<Loader2 className="size-4 animate-spin" />
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
) : showModelInput ? (
|
||||||
|
<Input
|
||||||
|
value={activeConfig.trackBlockModel}
|
||||||
|
onChange={(e) => updateConfig(provider, { trackBlockModel: e.target.value })}
|
||||||
|
placeholder={primaryModel || "Enter model"}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Select
|
||||||
|
value={activeConfig.trackBlockModel || "__same__"}
|
||||||
|
onValueChange={(value) => updateConfig(provider, { trackBlockModel: value === "__same__" ? "" : value })}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a model" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="__same__">Same as assistant</SelectItem>
|
||||||
|
{modelsForProvider.map((m) => (
|
||||||
|
<SelectItem key={m.id} value={m.id}>
|
||||||
|
{m.name || m.id}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* API Key */}
|
{/* API Key */}
|
||||||
|
|
|
||||||
|
|
@ -156,6 +156,8 @@ export function TrackModal() {
|
||||||
const lastRunAt = track?.lastRunAt ?? ''
|
const lastRunAt = track?.lastRunAt ?? ''
|
||||||
const lastRunId = track?.lastRunId ?? ''
|
const lastRunId = track?.lastRunId ?? ''
|
||||||
const lastRunSummary = track?.lastRunSummary ?? ''
|
const lastRunSummary = track?.lastRunSummary ?? ''
|
||||||
|
const model = track?.model ?? ''
|
||||||
|
const provider = track?.provider ?? ''
|
||||||
const scheduleSummary = useMemo(() => summarizeSchedule(schedule), [schedule])
|
const scheduleSummary = useMemo(() => summarizeSchedule(schedule), [schedule])
|
||||||
const triggerType: 'scheduled' | 'event' | 'manual' =
|
const triggerType: 'scheduled' | 'event' | 'manual' =
|
||||||
schedule ? 'scheduled' : eventMatchCriteria ? 'event' : 'manual'
|
schedule ? 'scheduled' : eventMatchCriteria ? 'event' : 'manual'
|
||||||
|
|
@ -393,6 +395,12 @@ export function TrackModal() {
|
||||||
<dt>Track ID</dt><dd><code>{trackId}</code></dd>
|
<dt>Track ID</dt><dd><code>{trackId}</code></dd>
|
||||||
<dt>File</dt><dd><code>{detail.filePath}</code></dd>
|
<dt>File</dt><dd><code>{detail.filePath}</code></dd>
|
||||||
<dt>Status</dt><dd>{active ? 'Active' : 'Paused'}</dd>
|
<dt>Status</dt><dd>{active ? 'Active' : 'Paused'}</dd>
|
||||||
|
{model && (<>
|
||||||
|
<dt>Model</dt><dd><code>{model}</code></dd>
|
||||||
|
</>)}
|
||||||
|
{provider && (<>
|
||||||
|
<dt>Provider</dt><dd><code>{provider}</code></dd>
|
||||||
|
</>)}
|
||||||
{lastRunAt && (<>
|
{lastRunAt && (<>
|
||||||
<dt>Last run</dt><dd>{formatDateTime(lastRunAt)}</dd>
|
<dt>Last run</dt><dd>{formatDateTime(lastRunAt)}</dd>
|
||||||
</>)}
|
</>)}
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,23 @@ ${schemaYaml}
|
||||||
|
|
||||||
**Runtime-managed fields — never write these yourself:** ` + "`" + `lastRunAt` + "`" + `, ` + "`" + `lastRunId` + "`" + `, ` + "`" + `lastRunSummary` + "`" + `.
|
**Runtime-managed fields — never write these yourself:** ` + "`" + `lastRunAt` + "`" + `, ` + "`" + `lastRunId` + "`" + `, ` + "`" + `lastRunSummary` + "`" + `.
|
||||||
|
|
||||||
|
## Do Not Set ` + "`" + `model` + "`" + ` or ` + "`" + `provider` + "`" + ` (almost always)
|
||||||
|
|
||||||
|
The schema includes optional ` + "`" + `model` + "`" + ` and ` + "`" + `provider` + "`" + ` fields. **Omit them.** A user-configurable global default already picks the right model and provider for tracks; setting per-track values bypasses that and is almost always wrong.
|
||||||
|
|
||||||
|
The only time these belong on a track:
|
||||||
|
|
||||||
|
- The user **explicitly** named a model or provider for *this specific track* in their request ("use Claude Opus for this one", "force this track onto OpenAI"). Quote the user's wording back when confirming.
|
||||||
|
|
||||||
|
Things that are **not** reasons to set these:
|
||||||
|
|
||||||
|
- "Tracks should be fast" / "I want a small model" — that's a global preference, not a per-track one. Leave it; the global default exists.
|
||||||
|
- "This track is complex" — write a clearer instruction; don't reach for a different model.
|
||||||
|
- "Just to be safe" / "in case it matters" — this is the antipattern. Leave them out.
|
||||||
|
- The user changed their main chat model — that has nothing to do with tracks. Leave them out.
|
||||||
|
|
||||||
|
When in doubt: omit both fields. Never volunteer them. Never include them in a starter template you suggest. If you find yourself adding them as a sensible default, stop — you're wrong.
|
||||||
|
|
||||||
## Choosing a trackId
|
## Choosing a trackId
|
||||||
|
|
||||||
- Kebab-case, short, descriptive: ` + "`" + `chicago-time` + "`" + `, ` + "`" + `sfo-weather` + "`" + `, ` + "`" + `hn-top5` + "`" + `, ` + "`" + `btc-usd` + "`" + `.
|
- Kebab-case, short, descriptive: ` + "`" + `chicago-time` + "`" + `, ` + "`" + `sfo-weather` + "`" + `, ` + "`" + `hn-top5` + "`" + `, ` + "`" + `btc-usd` + "`" + `.
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import path from 'path';
|
||||||
import { google } from 'googleapis';
|
import { google } from 'googleapis';
|
||||||
import { WorkDir } from '../config/config.js';
|
import { WorkDir } from '../config/config.js';
|
||||||
import { createRun, createMessage } from '../runs/runs.js';
|
import { createRun, createMessage } from '../runs/runs.js';
|
||||||
|
import { getKgModel } from '../models/defaults.js';
|
||||||
import { waitForRunCompletion } from '../agents/utils.js';
|
import { waitForRunCompletion } from '../agents/utils.js';
|
||||||
import { serviceLogger } from '../services/service_logger.js';
|
import { serviceLogger } from '../services/service_logger.js';
|
||||||
import { loadUserConfig, updateUserEmail } from '../config/user_config.js';
|
import { loadUserConfig, updateUserEmail } from '../config/user_config.js';
|
||||||
|
|
@ -305,7 +306,7 @@ async function processAgentNotes(): Promise<void> {
|
||||||
const timestamp = new Date().toISOString();
|
const timestamp = new Date().toISOString();
|
||||||
const message = `Current timestamp: ${timestamp}\n\nProcess the following source material and update the Agent Notes folder accordingly.\n\n${messageParts.join('\n\n')}`;
|
const message = `Current timestamp: ${timestamp}\n\nProcess the following source material and update the Agent Notes folder accordingly.\n\n${messageParts.join('\n\n')}`;
|
||||||
|
|
||||||
const agentRun = await createRun({ agentId: AGENT_ID });
|
const agentRun = await createRun({ agentId: AGENT_ID, model: await getKgModel() });
|
||||||
await createMessage(agentRun.id, message);
|
await createMessage(agentRun.id, message);
|
||||||
await waitForRunCompletion(agentRun.id);
|
await waitForRunCompletion(agentRun.id);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
export function getRaw(): string {
|
export function getRaw(): string {
|
||||||
return `---
|
return `---
|
||||||
model: anthropic/claude-haiku-4.5
|
|
||||||
tools:
|
tools:
|
||||||
workspace-writeFile:
|
workspace-writeFile:
|
||||||
type: builtin
|
type: builtin
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ export function getRaw(): string {
|
||||||
const defaultEndISO = defaultEnd.toISOString();
|
const defaultEndISO = defaultEnd.toISOString();
|
||||||
|
|
||||||
return `---
|
return `---
|
||||||
model: anthropic/claude-sonnet-4.6
|
|
||||||
tools:
|
tools:
|
||||||
${toolEntries}
|
${toolEntries}
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { CronExpressionParser } from 'cron-parser';
|
||||||
import { generateText } from 'ai';
|
import { generateText } from 'ai';
|
||||||
import { WorkDir } from '../config/config.js';
|
import { WorkDir } from '../config/config.js';
|
||||||
import { createRun, createMessage, fetchRun } from '../runs/runs.js';
|
import { createRun, createMessage, fetchRun } from '../runs/runs.js';
|
||||||
|
import { getKgModel } from '../models/defaults.js';
|
||||||
import container from '../di/container.js';
|
import container from '../di/container.js';
|
||||||
import type { IModelConfigRepo } from '../models/repo.js';
|
import type { IModelConfigRepo } from '../models/repo.js';
|
||||||
import { createProvider } from '../models/models.js';
|
import { createProvider } from '../models/models.js';
|
||||||
|
|
@ -467,7 +468,7 @@ async function processInlineTasks(): Promise<void> {
|
||||||
console.log(`[InlineTasks] Running task: "${task.instruction.slice(0, 80)}..."`);
|
console.log(`[InlineTasks] Running task: "${task.instruction.slice(0, 80)}..."`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const run = await createRun({ agentId: INLINE_TASK_AGENT });
|
const run = await createRun({ agentId: INLINE_TASK_AGENT, model: await getKgModel() });
|
||||||
|
|
||||||
const message = [
|
const message = [
|
||||||
`Execute the following instruction from the note "${relativePath}":`,
|
`Execute the following instruction from the note "${relativePath}":`,
|
||||||
|
|
@ -547,7 +548,7 @@ export async function processRowboatInstruction(
|
||||||
scheduleLabel: string | null;
|
scheduleLabel: string | null;
|
||||||
response: string | null;
|
response: string | null;
|
||||||
}> {
|
}> {
|
||||||
const run = await createRun({ agentId: INLINE_TASK_AGENT });
|
const run = await createRun({ agentId: INLINE_TASK_AGENT, model: await getKgModel() });
|
||||||
|
|
||||||
const message = [
|
const message = [
|
||||||
`Process the following @rowboat instruction from the note "${notePath}":`,
|
`Process the following @rowboat instruction from the note "${notePath}":`,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { WorkDir } from '../config/config.js';
|
import { WorkDir } from '../config/config.js';
|
||||||
import { createRun, createMessage } from '../runs/runs.js';
|
import { createRun, createMessage } from '../runs/runs.js';
|
||||||
|
import { getKgModel } from '../models/defaults.js';
|
||||||
import { bus } from '../runs/bus.js';
|
import { bus } from '../runs/bus.js';
|
||||||
import { waitForRunCompletion } from '../agents/utils.js';
|
import { waitForRunCompletion } from '../agents/utils.js';
|
||||||
import { serviceLogger } from '../services/service_logger.js';
|
import { serviceLogger } from '../services/service_logger.js';
|
||||||
|
|
@ -71,6 +72,7 @@ async function labelEmailBatch(
|
||||||
): Promise<{ runId: string; filesEdited: Set<string> }> {
|
): Promise<{ runId: string; filesEdited: Set<string> }> {
|
||||||
const run = await createRun({
|
const run = await createRun({
|
||||||
agentId: LABELING_AGENT,
|
agentId: LABELING_AGENT,
|
||||||
|
model: await getKgModel(),
|
||||||
});
|
});
|
||||||
|
|
||||||
let message = `Label the following ${files.length} email files by prepending YAML frontmatter.\n\n`;
|
let message = `Label the following ${files.length} email files by prepending YAML frontmatter.\n\n`;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { renderTagSystemForEmails } from './tag_system.js';
|
||||||
|
|
||||||
export function getRaw(): string {
|
export function getRaw(): string {
|
||||||
return `---
|
return `---
|
||||||
model: anthropic/claude-haiku-4.5
|
|
||||||
tools:
|
tools:
|
||||||
workspace-readFile:
|
workspace-readFile:
|
||||||
type: builtin
|
type: builtin
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import { renderNoteEffectRules } from './tag_system.js';
|
||||||
|
|
||||||
export function getRaw(): string {
|
export function getRaw(): string {
|
||||||
return `---
|
return `---
|
||||||
model: anthropic/claude-haiku-4.5
|
|
||||||
tools:
|
tools:
|
||||||
workspace-writeFile:
|
workspace-writeFile:
|
||||||
type: builtin
|
type: builtin
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { renderTagSystemForNotes } from './tag_system.js';
|
||||||
|
|
||||||
export function getRaw(): string {
|
export function getRaw(): string {
|
||||||
return `---
|
return `---
|
||||||
model: anthropic/claude-haiku-4.5
|
|
||||||
tools:
|
tools:
|
||||||
workspace-readFile:
|
workspace-readFile:
|
||||||
type: builtin
|
type: builtin
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { generateText } from 'ai';
|
import { generateText } from 'ai';
|
||||||
import { createProvider } from '../models/models.js';
|
import { createProvider } from '../models/models.js';
|
||||||
import { getDefaultModelAndProvider, resolveProviderConfig } from '../models/defaults.js';
|
import { getDefaultModelAndProvider, getMeetingNotesModel, resolveProviderConfig } from '../models/defaults.js';
|
||||||
import { WorkDir } from '../config/config.js';
|
import { WorkDir } from '../config/config.js';
|
||||||
|
|
||||||
const CALENDAR_SYNC_DIR = path.join(WorkDir, 'calendar_sync');
|
const CALENDAR_SYNC_DIR = path.join(WorkDir, 'calendar_sync');
|
||||||
|
|
@ -135,7 +135,8 @@ function loadCalendarEventContext(calendarEventJson: string): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function summarizeMeeting(transcript: string, meetingStartTime?: string, calendarEventJson?: string): Promise<string> {
|
export async function summarizeMeeting(transcript: string, meetingStartTime?: string, calendarEventJson?: string): Promise<string> {
|
||||||
const { model: modelId, provider: providerName } = await getDefaultModelAndProvider();
|
const modelId = await getMeetingNotesModel();
|
||||||
|
const { provider: providerName } = await getDefaultModelAndProvider();
|
||||||
const providerConfig = await resolveProviderConfig(providerName);
|
const providerConfig = await resolveProviderConfig(providerName);
|
||||||
const model = createProvider(providerConfig).languageModel(modelId);
|
const model = createProvider(providerConfig).languageModel(modelId);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { WorkDir } from '../config/config.js';
|
import { WorkDir } from '../config/config.js';
|
||||||
import { createRun, createMessage } from '../runs/runs.js';
|
import { createRun, createMessage } from '../runs/runs.js';
|
||||||
|
import { getKgModel } from '../models/defaults.js';
|
||||||
import { bus } from '../runs/bus.js';
|
import { bus } from '../runs/bus.js';
|
||||||
import { waitForRunCompletion } from '../agents/utils.js';
|
import { waitForRunCompletion } from '../agents/utils.js';
|
||||||
import { serviceLogger } from '../services/service_logger.js';
|
import { serviceLogger } from '../services/service_logger.js';
|
||||||
|
|
@ -84,6 +85,7 @@ async function tagNoteBatch(
|
||||||
): Promise<{ runId: string; filesEdited: Set<string> }> {
|
): Promise<{ runId: string; filesEdited: Set<string> }> {
|
||||||
const run = await createRun({
|
const run = await createRun({
|
||||||
agentId: NOTE_TAGGING_AGENT,
|
agentId: NOTE_TAGGING_AGENT,
|
||||||
|
model: await getKgModel(),
|
||||||
});
|
});
|
||||||
|
|
||||||
let message = `Tag the following ${files.length} knowledge notes by prepending YAML frontmatter with appropriate tags.\n\n`;
|
let message = `Tag the following ${files.length} knowledge notes by prepending YAML frontmatter with appropriate tags.\n\n`;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { generateObject } from 'ai';
|
||||||
import { trackBlock, PrefixLogger } from '@x/shared';
|
import { trackBlock, PrefixLogger } from '@x/shared';
|
||||||
import type { KnowledgeEvent } from '@x/shared/dist/track-block.js';
|
import type { KnowledgeEvent } from '@x/shared/dist/track-block.js';
|
||||||
import { createProvider } from '../../models/models.js';
|
import { createProvider } from '../../models/models.js';
|
||||||
import { getDefaultModelAndProvider, resolveProviderConfig } from '../../models/defaults.js';
|
import { getDefaultModelAndProvider, getTrackBlockModel, resolveProviderConfig } from '../../models/defaults.js';
|
||||||
|
|
||||||
const log = new PrefixLogger('TrackRouting');
|
const log = new PrefixLogger('TrackRouting');
|
||||||
|
|
||||||
|
|
@ -34,7 +34,8 @@ Rules:
|
||||||
- For each candidate, return BOTH trackId and filePath exactly as given. trackIds are not globally unique.`;
|
- For each candidate, return BOTH trackId and filePath exactly as given. trackIds are not globally unique.`;
|
||||||
|
|
||||||
async function resolveModel() {
|
async function resolveModel() {
|
||||||
const { model, provider } = await getDefaultModelAndProvider();
|
const model = await getTrackBlockModel();
|
||||||
|
const { provider } = await getDefaultModelAndProvider();
|
||||||
const config = await resolveProviderConfig(provider);
|
const config = await resolveProviderConfig(provider);
|
||||||
return createProvider(config).languageModel(model);
|
return createProvider(config).languageModel(model);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import z from 'zod';
|
import z from 'zod';
|
||||||
import { fetchAll, updateTrackBlock } from './fileops.js';
|
import { fetchAll, updateTrackBlock } from './fileops.js';
|
||||||
import { createRun, createMessage } from '../../runs/runs.js';
|
import { createRun, createMessage } from '../../runs/runs.js';
|
||||||
|
import { getTrackBlockModel } from '../../models/defaults.js';
|
||||||
import { extractAgentResponse, waitForRunCompletion } from '../../agents/utils.js';
|
import { extractAgentResponse, waitForRunCompletion } from '../../agents/utils.js';
|
||||||
import { trackBus } from './bus.js';
|
import { trackBus } from './bus.js';
|
||||||
import type { TrackStateSchema } from './types.js';
|
import type { TrackStateSchema } from './types.js';
|
||||||
|
|
@ -101,8 +102,15 @@ export async function triggerTrackUpdate(
|
||||||
|
|
||||||
const contentBefore = track.content;
|
const contentBefore = track.content;
|
||||||
|
|
||||||
// Emit start event — runId is set after agent run is created
|
// Per-track model/provider overrides win when set; otherwise fall back
|
||||||
const agentRun = await createRun({ agentId: 'track-run' });
|
// to the configured trackBlockModel default and the run-creation
|
||||||
|
// provider default (signed-in: rowboat; BYOK: active provider).
|
||||||
|
const model = track.track.model ?? await getTrackBlockModel();
|
||||||
|
const agentRun = await createRun({
|
||||||
|
agentId: 'track-run',
|
||||||
|
model,
|
||||||
|
...(track.track.provider ? { provider: track.track.provider } : {}),
|
||||||
|
});
|
||||||
|
|
||||||
// Set lastRunAt and lastRunId immediately (before agent executes) so
|
// Set lastRunAt and lastRunId immediately (before agent executes) so
|
||||||
// the scheduler's next poll won't re-trigger this track.
|
// the scheduler's next poll won't re-trigger this track.
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import container from "../di/container.js";
|
||||||
|
|
||||||
const SIGNED_IN_DEFAULT_MODEL = "gpt-5.4";
|
const SIGNED_IN_DEFAULT_MODEL = "gpt-5.4";
|
||||||
const SIGNED_IN_DEFAULT_PROVIDER = "rowboat";
|
const SIGNED_IN_DEFAULT_PROVIDER = "rowboat";
|
||||||
|
const SIGNED_IN_KG_MODEL = "anthropic/claude-haiku-4.5";
|
||||||
|
const SIGNED_IN_TRACK_BLOCK_MODEL = "anthropic/claude-haiku-4.5";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The single source of truth for "what model+provider should we use when
|
* The single source of truth for "what model+provider should we use when
|
||||||
|
|
@ -51,3 +53,36 @@ export async function resolveProviderConfig(name: string): Promise<z.infer<typeo
|
||||||
}
|
}
|
||||||
throw new Error(`Provider '${name}' is referenced but not configured`);
|
throw new Error(`Provider '${name}' is referenced but not configured`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model used by knowledge-graph agents (note_creation, labeling_agent, etc.)
|
||||||
|
* when they're the top-level of a run. Signed-in: curated default.
|
||||||
|
* BYOK: user override (`knowledgeGraphModel`) or assistant model.
|
||||||
|
*/
|
||||||
|
export async function getKgModel(): Promise<string> {
|
||||||
|
if (await isSignedIn()) return SIGNED_IN_KG_MODEL;
|
||||||
|
const cfg = await container.resolve<IModelConfigRepo>("modelConfigRepo").getConfig();
|
||||||
|
return cfg.knowledgeGraphModel ?? cfg.model;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model used by track-block runner + routing classifier.
|
||||||
|
* Signed-in: curated default. BYOK: user override (`trackBlockModel`) or
|
||||||
|
* assistant model.
|
||||||
|
*/
|
||||||
|
export async function getTrackBlockModel(): Promise<string> {
|
||||||
|
if (await isSignedIn()) return SIGNED_IN_TRACK_BLOCK_MODEL;
|
||||||
|
const cfg = await container.resolve<IModelConfigRepo>("modelConfigRepo").getConfig();
|
||||||
|
return cfg.trackBlockModel ?? cfg.model;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model used by the meeting-notes summarizer. No special signed-in default —
|
||||||
|
* historically meetings used the assistant model. BYOK: user override
|
||||||
|
* (`meetingNotesModel`) or assistant model.
|
||||||
|
*/
|
||||||
|
export async function getMeetingNotesModel(): Promise<string> {
|
||||||
|
if (await isSignedIn()) return SIGNED_IN_DEFAULT_MODEL;
|
||||||
|
const cfg = await container.resolve<IModelConfigRepo>("modelConfigRepo").getConfig();
|
||||||
|
return cfg.meetingNotesModel ?? cfg.model;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ export class FSModelConfigRepo implements IModelConfigRepo {
|
||||||
models: config.models,
|
models: config.models,
|
||||||
knowledgeGraphModel: config.knowledgeGraphModel,
|
knowledgeGraphModel: config.knowledgeGraphModel,
|
||||||
meetingNotesModel: config.meetingNotesModel,
|
meetingNotesModel: config.meetingNotesModel,
|
||||||
|
trackBlockModel: config.trackBlockModel,
|
||||||
};
|
};
|
||||||
|
|
||||||
const toWrite = { ...config, providers: existingProviders };
|
const toWrite = { ...config, providers: existingProviders };
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
---
|
---
|
||||||
model: anthropic/claude-haiku-4.5
|
|
||||||
tools:
|
tools:
|
||||||
workspace-readFile:
|
workspace-readFile:
|
||||||
type: builtin
|
type: builtin
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
---
|
---
|
||||||
model: anthropic/claude-haiku-4.5
|
|
||||||
tools:
|
tools:
|
||||||
workspace-readFile:
|
workspace-readFile:
|
||||||
type: builtin
|
type: builtin
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { WorkDir } from '../config/config.js';
|
import { WorkDir } from '../config/config.js';
|
||||||
import { createRun, createMessage } from '../runs/runs.js';
|
import { createRun, createMessage } from '../runs/runs.js';
|
||||||
|
import { getKgModel } from '../models/defaults.js';
|
||||||
import { waitForRunCompletion } from '../agents/utils.js';
|
import { waitForRunCompletion } from '../agents/utils.js';
|
||||||
import {
|
import {
|
||||||
loadConfig,
|
loadConfig,
|
||||||
|
|
@ -41,6 +42,7 @@ async function runAgent(agentName: string): Promise<void> {
|
||||||
// The agent file is expected to be in the agents directory with the same name
|
// The agent file is expected to be in the agents directory with the same name
|
||||||
const run = await createRun({
|
const run = await createRun({
|
||||||
agentId: agentName,
|
agentId: agentName,
|
||||||
|
model: await getKgModel(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Build trigger message with user context
|
// Build trigger message with user context
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,9 @@ export const LlmModelConfig = z.object({
|
||||||
model: z.string().optional(),
|
model: z.string().optional(),
|
||||||
models: z.array(z.string()).optional(),
|
models: z.array(z.string()).optional(),
|
||||||
})).optional(),
|
})).optional(),
|
||||||
// Deprecated: per-run model+provider supersedes these. Kept on the schema so
|
// Per-category model overrides (BYOK only — signed-in users always get
|
||||||
// existing settings/onboarding UIs continue to compile until they're cleaned up.
|
// the curated gateway defaults). Read by helpers in core/models/defaults.ts.
|
||||||
knowledgeGraphModel: z.string().optional(),
|
knowledgeGraphModel: z.string().optional(),
|
||||||
meetingNotesModel: z.string().optional(),
|
meetingNotesModel: z.string().optional(),
|
||||||
|
trackBlockModel: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ export const TrackBlockSchema = z.object({
|
||||||
eventMatchCriteria: z.string().optional().describe('When set, this track participates in event-based triggering. Describe what kinds of events should consider this track for an update (e.g. "Emails about Q3 planning"). Omit to disable event triggers — the track will only run on schedule or manually.'),
|
eventMatchCriteria: z.string().optional().describe('When set, this track participates in event-based triggering. Describe what kinds of events should consider this track for an update (e.g. "Emails about Q3 planning"). Omit to disable event triggers — the track will only run on schedule or manually.'),
|
||||||
active: z.boolean().default(true).describe('Set false to pause without deleting'),
|
active: z.boolean().default(true).describe('Set false to pause without deleting'),
|
||||||
schedule: TrackScheduleSchema.optional(),
|
schedule: TrackScheduleSchema.optional(),
|
||||||
|
model: z.string().optional().describe('ADVANCED — leave unset. Per-track LLM model override (e.g. "anthropic/claude-sonnet-4.6"). Only set when the user explicitly asked for a specific model for THIS track. The global default already picks a tuned model for tracks; overriding usually makes things worse, not better.'),
|
||||||
|
provider: z.string().optional().describe('ADVANCED — leave unset. Per-track provider name override (e.g. "openai", "anthropic"). Only set when the user explicitly asked for a specific provider for THIS track. Almost always omitted; the global default flows through correctly.'),
|
||||||
lastRunAt: z.string().optional().describe('Runtime-managed — never write this yourself'),
|
lastRunAt: z.string().optional().describe('Runtime-managed — never write this yourself'),
|
||||||
lastRunId: z.string().optional().describe('Runtime-managed — never write this yourself'),
|
lastRunId: z.string().optional().describe('Runtime-managed — never write this yourself'),
|
||||||
lastRunSummary: z.string().optional().describe('Runtime-managed — never write this yourself'),
|
lastRunSummary: z.string().optional().describe('Runtime-managed — never write this yourself'),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue