tycono 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +5 -3
- package/package.json +1 -1
- package/src/api/src/create-server.ts +2 -0
- package/src/api/src/routes/speech.ts +171 -0
- package/src/api/src/services/preferences.ts +14 -0
- package/src/web/dist/assets/index-Ct9pM1_i.js +90 -0
- package/src/web/dist/assets/index-Dy8nGrfX.css +1 -0
- package/src/web/dist/assets/{preview-app-Sc4AUzat.js → preview-app-a8V0eihg.js} +1 -1
- package/src/web/dist/index.html +2 -2
- package/src/web/dist/assets/index-D9yk3c_h.css +0 -1
- package/src/web/dist/assets/index-DyBIwMMO.js +0 -90
package/bin/cli.js
CHANGED
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
5
6
|
|
|
6
7
|
const __filename = fileURLToPath(import.meta.url);
|
|
7
8
|
const __dirname = dirname(__filename);
|
|
8
|
-
const pkgRoot = join(__dirname, '..');
|
|
9
9
|
|
|
10
|
-
//
|
|
11
|
-
|
|
10
|
+
// Resolve tsx using createRequire from THIS file's location
|
|
11
|
+
// This traverses up node_modules correctly for both local and npx flat installs
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
const tsxApiPath = pathToFileURL(require.resolve('tsx/esm/api')).href;
|
|
12
14
|
const tsx = await import(tsxApiPath);
|
|
13
15
|
tsx.register();
|
|
14
16
|
|
package/package.json
CHANGED
|
@@ -24,6 +24,7 @@ import { getAllActivities, completeActivity } from './services/activity-tracker.
|
|
|
24
24
|
import { knowledgeRouter } from './routes/knowledge.js';
|
|
25
25
|
import { preferencesRouter } from './routes/preferences.js';
|
|
26
26
|
import { saveRouter } from './routes/save.js';
|
|
27
|
+
import { speechRouter } from './routes/speech.js';
|
|
27
28
|
import { importKnowledge } from './services/knowledge-importer.js';
|
|
28
29
|
import { AnthropicProvider, type LLMProvider } from './engine/llm-adapter.js';
|
|
29
30
|
import { readConfig } from './services/company-config.js';
|
|
@@ -132,6 +133,7 @@ export function createHttpServer(): http.Server {
|
|
|
132
133
|
app.use('/api/sessions', sessionsRouter);
|
|
133
134
|
app.use('/api/knowledge', knowledgeRouter);
|
|
134
135
|
app.use('/api/preferences', preferencesRouter);
|
|
136
|
+
app.use('/api/speech', speechRouter);
|
|
135
137
|
app.use('/api/save', saveRouter);
|
|
136
138
|
|
|
137
139
|
app.get('/api/health', (_req, res) => {
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* speech.ts — Ambient Speech LLM generation endpoint
|
|
3
|
+
*
|
|
4
|
+
* Generates contextual, persona-driven speech for idle roles.
|
|
5
|
+
* Uses Haiku for cost efficiency (~$0.0003/call).
|
|
6
|
+
*/
|
|
7
|
+
import { Router, Request, Response, NextFunction } from 'express';
|
|
8
|
+
import { COMPANY_ROOT } from '../services/file-reader.js';
|
|
9
|
+
import { buildOrgTree } from '../engine/index.js';
|
|
10
|
+
import { AnthropicProvider } from '../engine/llm-adapter.js';
|
|
11
|
+
|
|
12
|
+
export const speechRouter = Router();
|
|
13
|
+
|
|
14
|
+
// Lazy-init LLM provider (Haiku for cost efficiency)
|
|
15
|
+
let llm: AnthropicProvider | null = null;
|
|
16
|
+
function getLLM(): AnthropicProvider {
|
|
17
|
+
if (!llm) {
|
|
18
|
+
llm = new AnthropicProvider({
|
|
19
|
+
model: process.env.SPEECH_MODEL || 'claude-haiku-4-5-20251001',
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return llm;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* POST /api/speech/generate
|
|
27
|
+
*
|
|
28
|
+
* Body: { roleId, context?: string, relationships?: Array<{ partnerId, familiarity }> }
|
|
29
|
+
* Returns: { speech: string, tokens: { input: number, output: number } }
|
|
30
|
+
*/
|
|
31
|
+
speechRouter.post('/generate', async (req: Request, res: Response, next: NextFunction) => {
|
|
32
|
+
try {
|
|
33
|
+
const { roleId, context, relationships } = req.body as {
|
|
34
|
+
roleId: string;
|
|
35
|
+
context?: string;
|
|
36
|
+
relationships?: Array<{ partnerId: string; partnerName: string; familiarity: number }>;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
if (!roleId) {
|
|
40
|
+
res.status(400).json({ error: 'roleId is required' });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Build org tree to get persona
|
|
45
|
+
const tree = buildOrgTree(COMPANY_ROOT);
|
|
46
|
+
const node = tree.nodes.get(roleId);
|
|
47
|
+
if (!node) {
|
|
48
|
+
res.status(404).json({ error: `Role not found: ${roleId}` });
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const persona = node.persona || `${node.name} (${node.level})`;
|
|
53
|
+
const relContext = relationships?.length
|
|
54
|
+
? `\nColleague relationships:\n${relationships.map(r =>
|
|
55
|
+
`- ${r.partnerName}: familiarity ${r.familiarity}/100`
|
|
56
|
+
).join('\n')}`
|
|
57
|
+
: '';
|
|
58
|
+
|
|
59
|
+
const systemPrompt = `You are ${node.name}, a ${node.level} employee at a tech company.
|
|
60
|
+
Your persona: ${persona}
|
|
61
|
+
|
|
62
|
+
Generate a brief, natural idle thought or mumble (1 sentence, max 30 characters in Korean).
|
|
63
|
+
This is what you'd say to yourself while sitting at your desk.
|
|
64
|
+
It should reflect your personality, current concerns, or professional interests.
|
|
65
|
+
Do NOT use quotes. Just output the raw sentence.
|
|
66
|
+
${relContext}
|
|
67
|
+
${context ? `\nCurrent situation: ${context}` : ''}`;
|
|
68
|
+
|
|
69
|
+
const provider = getLLM();
|
|
70
|
+
const response = await provider.chat(
|
|
71
|
+
systemPrompt,
|
|
72
|
+
[{ role: 'user', content: 'Generate one idle thought.' }],
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const text = response.content
|
|
76
|
+
.filter(c => c.type === 'text')
|
|
77
|
+
.map(c => (c as { type: 'text'; text: string }).text)
|
|
78
|
+
.join('')
|
|
79
|
+
.trim()
|
|
80
|
+
.replace(/^["']|["']$/g, ''); // strip quotes if LLM adds them
|
|
81
|
+
|
|
82
|
+
res.json({
|
|
83
|
+
speech: text,
|
|
84
|
+
tokens: response.usage,
|
|
85
|
+
});
|
|
86
|
+
} catch (err) {
|
|
87
|
+
next(err);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* POST /api/speech/conversation
|
|
93
|
+
*
|
|
94
|
+
* Body: { roleA, roleB, familiarity, context? }
|
|
95
|
+
* Returns: { turns: Array<{ speaker: string, text: string }>, tokens }
|
|
96
|
+
*/
|
|
97
|
+
speechRouter.post('/conversation', async (req: Request, res: Response, next: NextFunction) => {
|
|
98
|
+
try {
|
|
99
|
+
const { roleA, roleB, familiarity, context } = req.body as {
|
|
100
|
+
roleA: string;
|
|
101
|
+
roleB: string;
|
|
102
|
+
familiarity: number;
|
|
103
|
+
context?: string;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
if (!roleA || !roleB) {
|
|
107
|
+
res.status(400).json({ error: 'roleA and roleB are required' });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const tree = buildOrgTree(COMPANY_ROOT);
|
|
112
|
+
const nodeA = tree.nodes.get(roleA);
|
|
113
|
+
const nodeB = tree.nodes.get(roleB);
|
|
114
|
+
if (!nodeA || !nodeB) {
|
|
115
|
+
res.status(404).json({ error: 'Role not found' });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const famLevel = familiarity >= 80 ? 'best friends'
|
|
120
|
+
: familiarity >= 50 ? 'close colleagues'
|
|
121
|
+
: familiarity >= 20 ? 'coworkers'
|
|
122
|
+
: 'barely acquainted';
|
|
123
|
+
|
|
124
|
+
const relation = nodeA.reportsTo === roleB ? `${nodeA.name} reports to ${nodeB.name}`
|
|
125
|
+
: nodeB.reportsTo === roleA ? `${nodeB.name} reports to ${nodeA.name}`
|
|
126
|
+
: nodeA.level === 'c-level' && nodeB.level === 'c-level' ? 'C-level peers'
|
|
127
|
+
: 'colleagues';
|
|
128
|
+
|
|
129
|
+
const systemPrompt = `Generate a short office conversation between two employees (2-3 turns, each turn max 25 Korean characters).
|
|
130
|
+
|
|
131
|
+
${nodeA.name} (${nodeA.level}): ${nodeA.persona || 'a professional'}
|
|
132
|
+
${nodeB.name} (${nodeB.level}): ${nodeB.persona || 'a professional'}
|
|
133
|
+
|
|
134
|
+
They are ${relation}. Familiarity level: ${famLevel} (${familiarity}/100).
|
|
135
|
+
${context ? `Context: ${context}` : ''}
|
|
136
|
+
|
|
137
|
+
Output as JSON array: [{"speaker":"A","text":"..."},{"speaker":"B","text":"..."}]
|
|
138
|
+
No markdown, no quotes around the JSON. Just the array.`;
|
|
139
|
+
|
|
140
|
+
const provider = getLLM();
|
|
141
|
+
const response = await provider.chat(
|
|
142
|
+
systemPrompt,
|
|
143
|
+
[{ role: 'user', content: 'Generate the conversation.' }],
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const raw = response.content
|
|
147
|
+
.filter(c => c.type === 'text')
|
|
148
|
+
.map(c => (c as { type: 'text'; text: string }).text)
|
|
149
|
+
.join('')
|
|
150
|
+
.trim();
|
|
151
|
+
|
|
152
|
+
let turns: Array<{ speaker: string; text: string }>;
|
|
153
|
+
try {
|
|
154
|
+
turns = JSON.parse(raw);
|
|
155
|
+
} catch {
|
|
156
|
+
// Fallback: try to extract JSON from markdown code block
|
|
157
|
+
const match = raw.match(/\[[\s\S]*\]/);
|
|
158
|
+
turns = match ? JSON.parse(match[0]) : [
|
|
159
|
+
{ speaker: 'A', text: '...' },
|
|
160
|
+
{ speaker: 'B', text: '...' },
|
|
161
|
+
];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
res.json({
|
|
165
|
+
turns,
|
|
166
|
+
tokens: response.usage,
|
|
167
|
+
});
|
|
168
|
+
} catch (err) {
|
|
169
|
+
next(err);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
@@ -15,9 +15,19 @@ export interface CharacterAppearance {
|
|
|
15
15
|
shoeColor: string;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
export interface SpeechSettings {
|
|
19
|
+
/** 'template' = static pool only, 'llm' = AI generation, 'auto' = detect engine */
|
|
20
|
+
mode: 'template' | 'llm' | 'auto';
|
|
21
|
+
/** Interval between ambient speech in seconds */
|
|
22
|
+
intervalSec: number;
|
|
23
|
+
/** Daily budget for LLM speech in USD (0 = unlimited) */
|
|
24
|
+
dailyBudgetUsd: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
18
27
|
export interface Preferences {
|
|
19
28
|
appearances: Record<string, CharacterAppearance>;
|
|
20
29
|
theme: string;
|
|
30
|
+
speech?: SpeechSettings;
|
|
21
31
|
}
|
|
22
32
|
|
|
23
33
|
const CONFIG_DIR = '.tycono';
|
|
@@ -37,6 +47,7 @@ export function readPreferences(companyRoot: string): Preferences {
|
|
|
37
47
|
return {
|
|
38
48
|
appearances: data.appearances ?? {},
|
|
39
49
|
theme: data.theme ?? 'default',
|
|
50
|
+
speech: data.speech ?? undefined,
|
|
40
51
|
};
|
|
41
52
|
} catch {
|
|
42
53
|
return { ...DEFAULT, appearances: {} };
|
|
@@ -58,6 +69,9 @@ export function mergePreferences(companyRoot: string, partial: Partial<Preferenc
|
|
|
58
69
|
? { ...current.appearances, ...partial.appearances }
|
|
59
70
|
: current.appearances,
|
|
60
71
|
theme: partial.theme ?? current.theme,
|
|
72
|
+
speech: partial.speech !== undefined
|
|
73
|
+
? { ...current.speech, ...partial.speech }
|
|
74
|
+
: current.speech,
|
|
61
75
|
};
|
|
62
76
|
writePreferences(companyRoot, merged);
|
|
63
77
|
return merged;
|