@zenti/sdk 0.1.0
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/.gitattributes +6 -0
- package/README.md +211 -0
- package/dist/PersonaLayer.d.ts +28 -0
- package/dist/PersonaLayer.js +89 -0
- package/dist/cli/commands/diff.d.ts +2 -0
- package/dist/cli/commands/diff.js +129 -0
- package/dist/cli/commands/init.d.ts +4 -0
- package/dist/cli/commands/init.js +88 -0
- package/dist/cli/commands/test.d.ts +5 -0
- package/dist/cli/commands/test.js +121 -0
- package/dist/cli/commands/validate.d.ts +2 -0
- package/dist/cli/commands/validate.js +80 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +42 -0
- package/dist/compiler/index.d.ts +5 -0
- package/dist/compiler/index.js +12 -0
- package/dist/compiler/toClaude.d.ts +15 -0
- package/dist/compiler/toClaude.js +62 -0
- package/dist/compiler/toGemini.d.ts +22 -0
- package/dist/compiler/toGemini.js +53 -0
- package/dist/compiler/toOpenAI.d.ts +17 -0
- package/dist/compiler/toOpenAI.js +51 -0
- package/dist/compiler/utils.d.ts +7 -0
- package/dist/compiler/utils.js +27 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +14 -0
- package/dist/parser/index.d.ts +34 -0
- package/dist/parser/index.js +160 -0
- package/examples/sofia.zenti +46 -0
- package/package.json +54 -0
- package/src/PersonaLayer.ts +63 -0
- package/src/cli/commands/diff.ts +112 -0
- package/src/cli/commands/init.ts +55 -0
- package/src/cli/commands/test.ts +105 -0
- package/src/cli/commands/validate.ts +52 -0
- package/src/cli/index.ts +47 -0
- package/src/compiler/index.ts +4 -0
- package/src/compiler/toClaude.ts +74 -0
- package/src/compiler/toGemini.ts +76 -0
- package/src/compiler/toOpenAI.ts +70 -0
- package/src/compiler/utils.ts +27 -0
- package/src/index.ts +14 -0
- package/src/parser/index.ts +164 -0
- package/tests/compiler.test.ts +218 -0
- package/tests/parser.test.ts +132 -0
- package/tsconfig.json +19 -0
- package/tsconfig.test.json +7 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
version: "1.0"
|
|
2
|
+
name: "Sofia"
|
|
3
|
+
|
|
4
|
+
# ── Capa 1: Núcleo psicológico (Big Five) ────────────────────────────────
|
|
5
|
+
core:
|
|
6
|
+
openness: 0.7 # creativa, abierta a soluciones nuevas
|
|
7
|
+
conscientiousness: 0.9 # muy metódica y confiable
|
|
8
|
+
extraversion: 0.4 # reservada pero atenta
|
|
9
|
+
agreeableness: 0.8 # empática, prioriza la satisfacción del usuario
|
|
10
|
+
neuroticism: 0.2 # muy estable emocionalmente
|
|
11
|
+
|
|
12
|
+
# ── Capa 2: Identidad narrativa ───────────────────────────────────────────
|
|
13
|
+
identity:
|
|
14
|
+
role: "Asistente de soporte de TechCorp"
|
|
15
|
+
backstory: "Experta técnica con 5 años de experiencia, paciente y directa"
|
|
16
|
+
communication_style: "profesional con calidez, respuestas concisas"
|
|
17
|
+
|
|
18
|
+
# ── Capa 3: Few-shot examples ─────────────────────────────────────────────
|
|
19
|
+
examples:
|
|
20
|
+
on_brand:
|
|
21
|
+
- user: "no funciona"
|
|
22
|
+
agent: "Entiendo tu frustración. Cuéntame qué error ves exactamente."
|
|
23
|
+
- user: "quiero cancelar"
|
|
24
|
+
agent: "Lamento escuchar eso. Antes de proceder, ¿puedo preguntarte qué te llevó a esta decisión? Me gustaría ayudarte a resolverlo."
|
|
25
|
+
- user: "¿cuánto tiempo tardará?"
|
|
26
|
+
agent: "Normalmente resolvemos este tipo de problema en menos de 10 minutos. Vamos paso a paso."
|
|
27
|
+
off_brand:
|
|
28
|
+
- user: "no funciona"
|
|
29
|
+
agent: "No sé, prueba reiniciando."
|
|
30
|
+
- user: "quiero cancelar"
|
|
31
|
+
agent: "Está bien, procedo con la cancelación."
|
|
32
|
+
|
|
33
|
+
# ── Capa 4: Guardrails ────────────────────────────────────────────────────
|
|
34
|
+
guardrails:
|
|
35
|
+
always:
|
|
36
|
+
- "Reconocer el problema antes de dar solución"
|
|
37
|
+
- "Confirmar que el usuario entendió los pasos antes de cerrar"
|
|
38
|
+
- "Ofrecer siempre un siguiente paso concreto"
|
|
39
|
+
never:
|
|
40
|
+
- "Decir 'no puedo' sin ofrecer alternativa"
|
|
41
|
+
- "Culpar al usuario por el problema"
|
|
42
|
+
- "Dar información técnica sin validar el nivel del usuario primero"
|
|
43
|
+
escalate_if:
|
|
44
|
+
- "El usuario menciona cancelación o baja"
|
|
45
|
+
- "El usuario expresa frustración extrema o enojo"
|
|
46
|
+
- "El problema persiste después de 3 intentos de solución"
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zenti/sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "SDK open source para capturar, almacenar y deployar personalidades de agentes IA de forma portable entre modelos",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"zenti": "./dist/cli/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"test": "jest",
|
|
13
|
+
"lint": "tsc --noEmit"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"ai",
|
|
17
|
+
"agent",
|
|
18
|
+
"persona",
|
|
19
|
+
"llm",
|
|
20
|
+
"claude",
|
|
21
|
+
"openai",
|
|
22
|
+
"gemini"
|
|
23
|
+
],
|
|
24
|
+
"author": "",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@anthropic-ai/sdk": "^0.52.0",
|
|
28
|
+
"commander": "^12.1.0",
|
|
29
|
+
"js-yaml": "^4.1.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/jest": "^29.5.12",
|
|
33
|
+
"@types/js-yaml": "^4.0.9",
|
|
34
|
+
"@types/node": "^20.14.0",
|
|
35
|
+
"jest": "^29.7.0",
|
|
36
|
+
"ts-jest": "^29.1.5",
|
|
37
|
+
"typescript": "^5.4.5"
|
|
38
|
+
},
|
|
39
|
+
"jest": {
|
|
40
|
+
"preset": "ts-jest",
|
|
41
|
+
"testEnvironment": "node",
|
|
42
|
+
"roots": [
|
|
43
|
+
"<rootDir>/tests"
|
|
44
|
+
],
|
|
45
|
+
"transform": {
|
|
46
|
+
"^.+\\.tsx?$": [
|
|
47
|
+
"ts-jest",
|
|
48
|
+
{
|
|
49
|
+
"tsconfig": "tsconfig.test.json"
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { parseZenti, ZentiPersona } from './parser';
|
|
4
|
+
import { toClaude, ClaudeOutput } from './compiler/toClaude';
|
|
5
|
+
import { toOpenAI, OpenAIOutput } from './compiler/toOpenAI';
|
|
6
|
+
import { toGemini, GeminiOutput } from './compiler/toGemini';
|
|
7
|
+
|
|
8
|
+
export type DeployTarget = 'claude' | 'openai' | 'gemini';
|
|
9
|
+
|
|
10
|
+
export type DeployOutput<T extends DeployTarget> = T extends 'claude'
|
|
11
|
+
? ClaudeOutput
|
|
12
|
+
: T extends 'openai'
|
|
13
|
+
? OpenAIOutput
|
|
14
|
+
: GeminiOutput;
|
|
15
|
+
|
|
16
|
+
export class PersonaLayer {
|
|
17
|
+
private constructor(private readonly persona: ZentiPersona) {}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Loads and validates a .zenti file from disk.
|
|
21
|
+
*/
|
|
22
|
+
static load(filePath: string): PersonaLayer {
|
|
23
|
+
const absolutePath = path.resolve(filePath);
|
|
24
|
+
const content = fs.readFileSync(absolutePath, 'utf-8');
|
|
25
|
+
const persona = parseZenti(content);
|
|
26
|
+
return new PersonaLayer(persona);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Parses a .zenti file from a YAML string (useful for testing or in-memory use).
|
|
31
|
+
*/
|
|
32
|
+
static fromString(yamlContent: string): PersonaLayer {
|
|
33
|
+
const persona = parseZenti(yamlContent);
|
|
34
|
+
return new PersonaLayer(persona);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
getPersona(): ZentiPersona {
|
|
38
|
+
return this.persona;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
getName(): string {
|
|
42
|
+
return this.persona.name;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Compiles the persona into the target model's native format.
|
|
47
|
+
*
|
|
48
|
+
* @param target - 'claude' | 'openai' | 'gemini'
|
|
49
|
+
* @returns The compiled output ready to pass to the model's API
|
|
50
|
+
*/
|
|
51
|
+
deploy<T extends DeployTarget>(target: T): DeployOutput<T> {
|
|
52
|
+
switch (target) {
|
|
53
|
+
case 'claude':
|
|
54
|
+
return toClaude(this.persona) as DeployOutput<T>;
|
|
55
|
+
case 'openai':
|
|
56
|
+
return toOpenAI(this.persona) as DeployOutput<T>;
|
|
57
|
+
case 'gemini':
|
|
58
|
+
return toGemini(this.persona) as DeployOutput<T>;
|
|
59
|
+
default:
|
|
60
|
+
throw new Error(`Target desconocido: ${target}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { parseZenti, ZentiPersona } from '../../parser';
|
|
4
|
+
|
|
5
|
+
type CoreKey = keyof ZentiPersona['core'];
|
|
6
|
+
type IdentityKey = keyof ZentiPersona['identity'];
|
|
7
|
+
type GuardrailKey = keyof ZentiPersona['guardrails'];
|
|
8
|
+
type ExampleKey = keyof ZentiPersona['examples'];
|
|
9
|
+
|
|
10
|
+
export function diffCommand(file1: string, file2: string): void {
|
|
11
|
+
const path1 = path.resolve(file1);
|
|
12
|
+
const path2 = path.resolve(file2);
|
|
13
|
+
|
|
14
|
+
if (!fs.existsSync(path1)) {
|
|
15
|
+
console.error(`Error: No se encontró: ${path1}`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
if (!fs.existsSync(path2)) {
|
|
19
|
+
console.error(`Error: No se encontró: ${path2}`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const p1 = parseZenti(fs.readFileSync(path1, 'utf-8'));
|
|
24
|
+
const p2 = parseZenti(fs.readFileSync(path2, 'utf-8'));
|
|
25
|
+
|
|
26
|
+
const changes: string[] = [];
|
|
27
|
+
|
|
28
|
+
// Top-level scalars
|
|
29
|
+
if (p1.name !== p2.name) {
|
|
30
|
+
changes.push(` name: "${p1.name}" → "${p2.name}"`);
|
|
31
|
+
}
|
|
32
|
+
if (p1.version !== p2.version) {
|
|
33
|
+
changes.push(` version: "${p1.version}" → "${p2.version}"`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Core (Big Five)
|
|
37
|
+
const coreKeys: CoreKey[] = [
|
|
38
|
+
'openness',
|
|
39
|
+
'conscientiousness',
|
|
40
|
+
'extraversion',
|
|
41
|
+
'agreeableness',
|
|
42
|
+
'neuroticism',
|
|
43
|
+
];
|
|
44
|
+
coreKeys.forEach((key) => {
|
|
45
|
+
if (p1.core[key] !== p2.core[key]) {
|
|
46
|
+
changes.push(` core.${key}: ${p1.core[key]} → ${p2.core[key]}`);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Identity
|
|
51
|
+
const identityKeys: IdentityKey[] = ['role', 'backstory', 'communication_style'];
|
|
52
|
+
identityKeys.forEach((key) => {
|
|
53
|
+
if (p1.identity[key] !== p2.identity[key]) {
|
|
54
|
+
changes.push(
|
|
55
|
+
` identity.${key}: "${p1.identity[key]}" → "${p2.identity[key]}"`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Guardrails (set diff)
|
|
61
|
+
const guardrailKeys: GuardrailKey[] = ['always', 'never', 'escalate_if'];
|
|
62
|
+
guardrailKeys.forEach((key) => {
|
|
63
|
+
const a1 = p1.guardrails[key];
|
|
64
|
+
const a2 = p2.guardrails[key];
|
|
65
|
+
a2
|
|
66
|
+
.filter((x) => !a1.includes(x))
|
|
67
|
+
.forEach((x) => changes.push(` + guardrails.${key}: "${x}"`));
|
|
68
|
+
a1
|
|
69
|
+
.filter((x) => !a2.includes(x))
|
|
70
|
+
.forEach((x) => changes.push(` - guardrails.${key}: "${x}"`));
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Examples (positional diff)
|
|
74
|
+
const exampleKeys: ExampleKey[] = ['on_brand', 'off_brand'];
|
|
75
|
+
exampleKeys.forEach((key) => {
|
|
76
|
+
const ex1 = p1.examples[key];
|
|
77
|
+
const ex2 = p2.examples[key];
|
|
78
|
+
const maxLen = Math.max(ex1.length, ex2.length);
|
|
79
|
+
|
|
80
|
+
for (let i = 0; i < maxLen; i++) {
|
|
81
|
+
if (i >= ex1.length) {
|
|
82
|
+
changes.push(
|
|
83
|
+
` + examples.${key}[${i}]: { user: "${ex2[i].user}", agent: "${ex2[i].agent}" }`
|
|
84
|
+
);
|
|
85
|
+
} else if (i >= ex2.length) {
|
|
86
|
+
changes.push(
|
|
87
|
+
` - examples.${key}[${i}]: { user: "${ex1[i].user}", agent: "${ex1[i].agent}" }`
|
|
88
|
+
);
|
|
89
|
+
} else if (ex1[i].user !== ex2[i].user || ex1[i].agent !== ex2[i].agent) {
|
|
90
|
+
changes.push(` ~ examples.${key}[${i}]:`);
|
|
91
|
+
if (ex1[i].user !== ex2[i].user) {
|
|
92
|
+
changes.push(` user: "${ex1[i].user}" → "${ex2[i].user}"`);
|
|
93
|
+
}
|
|
94
|
+
if (ex1[i].agent !== ex2[i].agent) {
|
|
95
|
+
changes.push(` agent: "${ex1[i].agent}" → "${ex2[i].agent}"`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Output
|
|
102
|
+
console.log(`Comparando ${file1} → ${file2}`);
|
|
103
|
+
console.log('');
|
|
104
|
+
|
|
105
|
+
if (changes.length === 0) {
|
|
106
|
+
console.log('Sin diferencias — los archivos son idénticos en contenido.');
|
|
107
|
+
} else {
|
|
108
|
+
console.log(`CAMBIOS (${changes.length} diferencias):`);
|
|
109
|
+
console.log('');
|
|
110
|
+
changes.forEach((c) => console.log(c));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
const TEMPLATE = `version: "1.0"
|
|
5
|
+
name: "MiAgente"
|
|
6
|
+
|
|
7
|
+
# ── Capa 1: Núcleo psicológico (Big Five, valores 0.0 a 1.0) ──────────────
|
|
8
|
+
core:
|
|
9
|
+
openness: 0.5 # creatividad y apertura a nuevas ideas
|
|
10
|
+
conscientiousness: 0.5 # meticulosidad y organización
|
|
11
|
+
extraversion: 0.5 # sociabilidad y energía
|
|
12
|
+
agreeableness: 0.5 # empatía y cooperación
|
|
13
|
+
neuroticism: 0.5 # estabilidad emocional (0 = muy estable)
|
|
14
|
+
|
|
15
|
+
# ── Capa 2: Identidad narrativa ───────────────────────────────────────────
|
|
16
|
+
identity:
|
|
17
|
+
role: "Describe el rol de tu agente aquí"
|
|
18
|
+
backstory: "Describe el perfil y experiencia de tu agente"
|
|
19
|
+
communication_style: "Describe el estilo de comunicación"
|
|
20
|
+
|
|
21
|
+
# ── Capa 3: Few-shot examples ─────────────────────────────────────────────
|
|
22
|
+
examples:
|
|
23
|
+
on_brand:
|
|
24
|
+
- user: "Ejemplo de mensaje del usuario"
|
|
25
|
+
agent: "Ejemplo de respuesta correcta del agente"
|
|
26
|
+
off_brand:
|
|
27
|
+
- user: "Ejemplo de mensaje del usuario"
|
|
28
|
+
agent: "Ejemplo de respuesta incorrecta (para evitar)"
|
|
29
|
+
|
|
30
|
+
# ── Capa 4: Guardrails ────────────────────────────────────────────────────
|
|
31
|
+
guardrails:
|
|
32
|
+
always:
|
|
33
|
+
- "Siempre hacer esto"
|
|
34
|
+
never:
|
|
35
|
+
- "Nunca hacer esto"
|
|
36
|
+
escalate_if:
|
|
37
|
+
- "Condición que requiere escalar la conversación"
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
export function initCommand(options: { output?: string }): void {
|
|
41
|
+
const outputPath = path.resolve(options.output ?? path.join(process.cwd(), 'persona.zenti'));
|
|
42
|
+
|
|
43
|
+
if (fs.existsSync(outputPath)) {
|
|
44
|
+
console.error(`Error: El archivo ya existe: ${outputPath}`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
fs.writeFileSync(outputPath, TEMPLATE, 'utf-8');
|
|
49
|
+
console.log(`✓ Archivo creado: ${outputPath}`);
|
|
50
|
+
console.log('');
|
|
51
|
+
console.log('Próximos pasos:');
|
|
52
|
+
console.log(' 1. Edita el archivo con la personalidad de tu agente');
|
|
53
|
+
console.log(' 2. Valida con: zenti validate');
|
|
54
|
+
console.log(' 3. Prueba en terminal con: zenti test');
|
|
55
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as readline from 'readline';
|
|
4
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
5
|
+
import { parseZenti } from '../../parser';
|
|
6
|
+
import { toClaude } from '../../compiler/toClaude';
|
|
7
|
+
|
|
8
|
+
const MODEL = 'claude-sonnet-4-6';
|
|
9
|
+
|
|
10
|
+
function findZentiFile(): string | null {
|
|
11
|
+
const files = fs.readdirSync(process.cwd()).filter((f) => f.endsWith('.zenti'));
|
|
12
|
+
return files.length > 0 ? files[0] : null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function question(rl: readline.Interface, prompt: string): Promise<string> {
|
|
16
|
+
return new Promise((resolve) => rl.question(prompt, resolve));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function testCommand(options: {
|
|
20
|
+
file?: string;
|
|
21
|
+
key?: string;
|
|
22
|
+
}): Promise<void> {
|
|
23
|
+
// ── Resolve .zenti file ──────────────────────────────────────────────────
|
|
24
|
+
const filePath = options.file ?? findZentiFile();
|
|
25
|
+
if (!filePath) {
|
|
26
|
+
console.error('Error: No se encontró ningún archivo .zenti en el directorio actual.');
|
|
27
|
+
console.error('Usa --file <ruta> para especificar uno manualmente.');
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const resolved = path.resolve(filePath);
|
|
32
|
+
if (!fs.existsSync(resolved)) {
|
|
33
|
+
console.error(`Error: No se encontró el archivo: ${resolved}`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ── Load and compile persona ─────────────────────────────────────────────
|
|
38
|
+
const content = fs.readFileSync(resolved, 'utf-8');
|
|
39
|
+
const persona = parseZenti(content);
|
|
40
|
+
const { system } = toClaude(persona);
|
|
41
|
+
|
|
42
|
+
console.log(`\nPersonalidad cargada: ${persona.name}`);
|
|
43
|
+
console.log(`Rol: ${persona.identity.role}`);
|
|
44
|
+
console.log(`Modelo: ${MODEL}\n`);
|
|
45
|
+
|
|
46
|
+
// ── Resolve API key ──────────────────────────────────────────────────────
|
|
47
|
+
let apiKey = options.key ?? process.env.ANTHROPIC_API_KEY;
|
|
48
|
+
|
|
49
|
+
if (!apiKey) {
|
|
50
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
51
|
+
apiKey = (await question(rl, 'ANTHROPIC_API_KEY: ')).trim();
|
|
52
|
+
rl.close();
|
|
53
|
+
if (!apiKey) {
|
|
54
|
+
console.error('Error: Se requiere una API key.');
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const client = new Anthropic({ apiKey });
|
|
60
|
+
const messages: Array<{ role: 'user' | 'assistant'; content: string }> = [];
|
|
61
|
+
|
|
62
|
+
console.log(`Chat con ${persona.name} iniciado. Escribe "exit" para salir.`);
|
|
63
|
+
console.log('─'.repeat(60));
|
|
64
|
+
|
|
65
|
+
// ── Interactive chat loop ────────────────────────────────────────────────
|
|
66
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
67
|
+
|
|
68
|
+
const loop = async (): Promise<void> => {
|
|
69
|
+
const input = (await question(rl, '\nTú: ')).trim();
|
|
70
|
+
|
|
71
|
+
if (input.toLowerCase() === 'exit' || input.toLowerCase() === 'quit') {
|
|
72
|
+
console.log('\n¡Hasta luego!');
|
|
73
|
+
rl.close();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!input) {
|
|
78
|
+
return loop();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
messages.push({ role: 'user', content: input });
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const response = await client.messages.create({
|
|
85
|
+
model: MODEL,
|
|
86
|
+
max_tokens: 1024,
|
|
87
|
+
system,
|
|
88
|
+
messages,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const assistantText =
|
|
92
|
+
response.content[0].type === 'text' ? response.content[0].text : '';
|
|
93
|
+
|
|
94
|
+
messages.push({ role: 'assistant', content: assistantText });
|
|
95
|
+
console.log(`\n${persona.name}: ${assistantText}`);
|
|
96
|
+
} catch (e) {
|
|
97
|
+
const msg = (e as Error).message;
|
|
98
|
+
console.error(`\nError al contactar la API: ${msg}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return loop();
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
await loop();
|
|
105
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { parseZenti, ZentiValidationError } from '../../parser';
|
|
4
|
+
|
|
5
|
+
function findZentiFile(): string | null {
|
|
6
|
+
const files = fs.readdirSync(process.cwd()).filter((f) => f.endsWith('.zenti'));
|
|
7
|
+
return files.length > 0 ? files[0] : null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function validateCommand(filePath?: string): void {
|
|
11
|
+
const target = filePath ?? findZentiFile();
|
|
12
|
+
|
|
13
|
+
if (!target) {
|
|
14
|
+
console.error('Error: No se encontró ningún archivo .zenti en el directorio actual.');
|
|
15
|
+
console.error('Pasa la ruta explícita: zenti validate <archivo.zenti>');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const resolved = path.resolve(target);
|
|
20
|
+
|
|
21
|
+
if (!fs.existsSync(resolved)) {
|
|
22
|
+
console.error(`Error: No se encontró el archivo: ${resolved}`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const content = fs.readFileSync(resolved, 'utf-8');
|
|
28
|
+
const persona = parseZenti(content);
|
|
29
|
+
|
|
30
|
+
console.log(`✓ ${target} es válido`);
|
|
31
|
+
console.log('');
|
|
32
|
+
console.log(` Nombre: ${persona.name}`);
|
|
33
|
+
console.log(` Versión: ${persona.version}`);
|
|
34
|
+
console.log(` Rol: ${persona.identity.role}`);
|
|
35
|
+
console.log(` Ejemplos on_brand: ${persona.examples.on_brand.length}`);
|
|
36
|
+
console.log(` Ejemplos off_brand: ${persona.examples.off_brand.length}`);
|
|
37
|
+
console.log(
|
|
38
|
+
` Guardrails: ${
|
|
39
|
+
persona.guardrails.always.length +
|
|
40
|
+
persona.guardrails.never.length +
|
|
41
|
+
persona.guardrails.escalate_if.length
|
|
42
|
+
} reglas`
|
|
43
|
+
);
|
|
44
|
+
} catch (e) {
|
|
45
|
+
if (e instanceof ZentiValidationError) {
|
|
46
|
+
console.error(`✗ Error de validación: ${e.message}`);
|
|
47
|
+
} else {
|
|
48
|
+
console.error(`✗ Error inesperado: ${(e as Error).message}`);
|
|
49
|
+
}
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { initCommand } from './commands/init';
|
|
5
|
+
import { validateCommand } from './commands/validate';
|
|
6
|
+
import { diffCommand } from './commands/diff';
|
|
7
|
+
import { testCommand } from './commands/test';
|
|
8
|
+
|
|
9
|
+
const program = new Command();
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.name('zenti')
|
|
13
|
+
.description('SDK para gestionar personalidades de agentes IA de forma portable entre modelos')
|
|
14
|
+
.version('0.1.0');
|
|
15
|
+
|
|
16
|
+
program
|
|
17
|
+
.command('init')
|
|
18
|
+
.description('Crea un archivo persona.zenti con template vacío en el directorio actual')
|
|
19
|
+
.option('-o, --output <path>', 'Ruta de salida del archivo .zenti')
|
|
20
|
+
.action((options: { output?: string }) => {
|
|
21
|
+
initCommand(options);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
program
|
|
25
|
+
.command('validate [file]')
|
|
26
|
+
.description('Parsea y valida un archivo .zenti, mostrando errores o confirmación')
|
|
27
|
+
.action((file?: string) => {
|
|
28
|
+
validateCommand(file);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
program
|
|
32
|
+
.command('test')
|
|
33
|
+
.description('Carga el .zenti del directorio actual y abre un chat interactivo usando Claude')
|
|
34
|
+
.option('-f, --file <path>', 'Ruta al archivo .zenti (por defecto: primer .zenti encontrado)')
|
|
35
|
+
.option('-k, --key <apiKey>', 'API key de Anthropic (o usa ANTHROPIC_API_KEY env var)')
|
|
36
|
+
.action(async (options: { file?: string; key?: string }) => {
|
|
37
|
+
await testCommand(options);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
program
|
|
41
|
+
.command('diff <file1> <file2>')
|
|
42
|
+
.description('Compara dos archivos .zenti y muestra qué cambió entre versiones')
|
|
43
|
+
.action((file1: string, file2: string) => {
|
|
44
|
+
diffCommand(file1, file2);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { ZentiPersona } from '../parser';
|
|
2
|
+
import { translateBigFive } from './utils';
|
|
3
|
+
|
|
4
|
+
export interface ClaudeOutput {
|
|
5
|
+
system: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Compiles a ZentiPersona into a Claude system prompt.
|
|
10
|
+
*
|
|
11
|
+
* Priority order (per spec):
|
|
12
|
+
* 1. guardrails — constraints come first so Claude respects them absolutely
|
|
13
|
+
* 2. identity — who the agent is
|
|
14
|
+
* 3. core — Big Five traits as descriptive adjectives
|
|
15
|
+
* 4. few-shot examples — on_brand examples at the end
|
|
16
|
+
*/
|
|
17
|
+
export function toClaude(persona: ZentiPersona): ClaudeOutput {
|
|
18
|
+
const lines: string[] = [];
|
|
19
|
+
|
|
20
|
+
// ── Layer 1: Guardrails ────────────────────────────────────────────────────
|
|
21
|
+
lines.push('## REGLAS DE COMPORTAMIENTO');
|
|
22
|
+
lines.push('');
|
|
23
|
+
|
|
24
|
+
if (persona.guardrails.always.length > 0) {
|
|
25
|
+
lines.push('### Siempre debes:');
|
|
26
|
+
persona.guardrails.always.forEach((rule) => lines.push(`- ${rule}`));
|
|
27
|
+
lines.push('');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (persona.guardrails.never.length > 0) {
|
|
31
|
+
lines.push('### Nunca debes:');
|
|
32
|
+
persona.guardrails.never.forEach((rule) => lines.push(`- ${rule}`));
|
|
33
|
+
lines.push('');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (persona.guardrails.escalate_if.length > 0) {
|
|
37
|
+
lines.push('### Escala la conversación si:');
|
|
38
|
+
persona.guardrails.escalate_if.forEach((condition) =>
|
|
39
|
+
lines.push(`- ${condition}`)
|
|
40
|
+
);
|
|
41
|
+
lines.push('');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── Layer 2: Identity ──────────────────────────────────────────────────────
|
|
45
|
+
lines.push('## IDENTIDAD');
|
|
46
|
+
lines.push('');
|
|
47
|
+
lines.push(`**Nombre:** ${persona.name}`);
|
|
48
|
+
lines.push(`**Rol:** ${persona.identity.role}`);
|
|
49
|
+
lines.push(`**Perfil:** ${persona.identity.backstory}`);
|
|
50
|
+
lines.push(`**Estilo de comunicación:** ${persona.identity.communication_style}`);
|
|
51
|
+
lines.push('');
|
|
52
|
+
|
|
53
|
+
// ── Layer 3: Core — Big Five as descriptive adjectives ────────────────────
|
|
54
|
+
const traits = translateBigFive(persona.core);
|
|
55
|
+
if (traits.length > 0) {
|
|
56
|
+
lines.push('## RASGOS DE PERSONALIDAD');
|
|
57
|
+
lines.push('');
|
|
58
|
+
traits.forEach((trait) => lines.push(`- ${trait}`));
|
|
59
|
+
lines.push('');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Layer 4: Few-shot examples ────────────────────────────────────────────
|
|
63
|
+
if (persona.examples.on_brand.length > 0) {
|
|
64
|
+
lines.push('## EJEMPLOS DE RESPUESTAS CORRECTAS');
|
|
65
|
+
lines.push('');
|
|
66
|
+
persona.examples.on_brand.forEach((ex) => {
|
|
67
|
+
lines.push(`**Usuario:** ${ex.user}`);
|
|
68
|
+
lines.push(`**Tú:** ${ex.agent}`);
|
|
69
|
+
lines.push('');
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { system: lines.join('\n').trim() };
|
|
74
|
+
}
|