@far-world-labs/verblets 0.1.4 → 0.2.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.
Files changed (46) hide show
  1. package/.github/workflows/ci.yml +38 -43
  2. package/.vitest.config.examples.js +4 -0
  3. package/DEVELOPING.md +1 -1
  4. package/package.json +9 -9
  5. package/scripts/clear-redis.js +74 -0
  6. package/src/chains/conversation/README.md +26 -0
  7. package/src/chains/conversation/index.examples.js +398 -0
  8. package/src/chains/conversation/index.js +126 -0
  9. package/src/chains/conversation/index.spec.js +148 -0
  10. package/src/chains/conversation/turn-policies.js +93 -0
  11. package/src/chains/conversation/turn-policies.md +123 -0
  12. package/src/chains/conversation/turn-policies.spec.js +135 -0
  13. package/src/chains/expect/index.js +34 -0
  14. package/src/chains/intersections/README.md +20 -6
  15. package/src/chains/intersections/index.examples.js +9 -8
  16. package/src/chains/intersections/index.js +39 -187
  17. package/src/chains/llm-logger/README.md +291 -133
  18. package/src/chains/llm-logger/index.js +451 -65
  19. package/src/chains/llm-logger/index.spec.js +85 -24
  20. package/src/chains/llm-logger/schema.json +105 -0
  21. package/src/chains/set-interval/index.examples.js +34 -6
  22. package/src/chains/set-interval/index.js +53 -32
  23. package/src/chains/themes/index.js +2 -2
  24. package/src/constants/common.js +7 -1
  25. package/src/constants/models.js +21 -9
  26. package/src/index.js +14 -4
  27. package/src/lib/assert/README.md +84 -0
  28. package/src/lib/assert/index.js +50 -0
  29. package/src/lib/ring-buffer/README.md +50 -428
  30. package/src/lib/ring-buffer/index.js +148 -987
  31. package/src/lib/ring-buffer/index.spec.js +388 -0
  32. package/src/verblets/conversation-turn/README.md +33 -0
  33. package/src/verblets/conversation-turn/index.examples.js +218 -0
  34. package/src/verblets/conversation-turn/index.js +68 -0
  35. package/src/verblets/conversation-turn/index.spec.js +77 -0
  36. package/src/verblets/conversation-turn-multi/README.md +31 -0
  37. package/src/verblets/conversation-turn-multi/index.examples.js +160 -0
  38. package/src/verblets/conversation-turn-multi/index.js +104 -0
  39. package/src/verblets/conversation-turn-multi/index.spec.js +63 -0
  40. package/src/verblets/intent/index.examples.js +1 -1
  41. package/src/verblets/intersection/index.js +46 -5
  42. package/src/verblets/people-list/README.md +28 -0
  43. package/src/verblets/people-list/index.examples.js +184 -0
  44. package/src/verblets/people-list/index.js +44 -0
  45. package/src/verblets/people-list/index.spec.js +49 -0
  46. package/scripts/version-bump.js +0 -33
@@ -72,50 +72,11 @@ jobs:
72
72
  echo "✅ ESLint checks completed"
73
73
  echo "✅ Library is ready for deployment"
74
74
 
75
- # Version bump for PRs - creates commit in PR branch
76
- version-bump:
77
- name: 📦 Version Bump
78
- runs-on: ubuntu-latest
79
- needs: build
80
- if: github.event_name == 'pull_request'
81
- permissions:
82
- contents: write
83
- pull-requests: write
84
- steps:
85
- - uses: actions/checkout@v4
86
- with:
87
- fetch-depth: 0
88
- token: ${{ secrets.GITHUB_TOKEN }}
89
- ref: ${{ github.head_ref }}
90
- - uses: actions/setup-node@v4
91
- with:
92
- node-version: 20.x
93
- cache: 'npm'
94
- - run: npm ci
95
-
96
- - name: Configure Git
97
- run: |
98
- git config --global user.name "github-actions[bot]"
99
- git config --global user.email "github-actions[bot]@users.noreply.github.com"
100
-
101
- - name: Version Bump (No Publish)
102
- env:
103
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
104
- run: |
105
- # Ensure we're on the correct branch
106
- git checkout ${{ github.head_ref }}
107
-
108
- # Use release-it to bump version and create tag, but skip npm publish
109
- npx release-it --ci --no-npm.publish --no-github.release
110
-
111
- # Push the version bump back to the PR branch
112
- git push origin ${{ github.head_ref }}
113
-
114
75
  # Required status check that gates merge operations
115
76
  pr-ready-to-merge:
116
77
  name: ✅ PR Ready to Merge
117
78
  runs-on: ubuntu-latest
118
- needs: [lint, test, build, version-bump]
79
+ needs: [lint, test, build]
119
80
  if: github.event_name == 'pull_request'
120
81
  steps:
121
82
  - name: All checks passed
@@ -124,11 +85,11 @@ jobs:
124
85
  echo "✅ Linting: Passed"
125
86
  echo "✅ Tests: Passed on all LTS Node versions"
126
87
  echo "✅ Build: Successful"
127
- echo " Version: Bumped and ready"
88
+ echo "ℹ️ Version: Will be checked on merge (publish only if bumped)"
128
89
  echo ""
129
90
  echo "This PR is now ready for squash and merge."
130
91
 
131
- # Publish the already-bumped version on merge to main
92
+ # Publish to NPM and create git tag - only if version was bumped
132
93
  release:
133
94
  name: 🚀 Publish to NPM
134
95
  runs-on: ubuntu-latest
@@ -149,14 +110,42 @@ jobs:
149
110
  cache: 'npm'
150
111
  - run: npm ci
151
112
 
113
+ - name: Check if version was bumped
114
+ id: version-check
115
+ run: |
116
+ CURRENT_VERSION=$(node -p "require('./package.json').version")
117
+ PUBLISHED_VERSION=$(npm view @far-world-labs/verblets version 2>/dev/null || echo "0.0.0")
118
+
119
+ echo "📦 Current version in package.json: $CURRENT_VERSION"
120
+ echo "📦 Published version on npm: $PUBLISHED_VERSION"
121
+
122
+ if [ "$CURRENT_VERSION" = "$PUBLISHED_VERSION" ]; then
123
+ echo "should-publish=false" >> $GITHUB_OUTPUT
124
+ echo "ℹ️ No version bump detected - skipping publish"
125
+ else
126
+ echo "should-publish=true" >> $GITHUB_OUTPUT
127
+ echo "✅ Version bump detected: $PUBLISHED_VERSION → $CURRENT_VERSION - will publish"
128
+ fi
129
+
130
+ - name: Create Git Tag
131
+ if: steps.version-check.outputs.should-publish == 'true'
132
+ run: |
133
+ VERSION=$(node -p "require('./package.json').version")
134
+ git config --global user.name "github-actions[bot]"
135
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
136
+ git tag -a "v$VERSION" -m "Release v$VERSION"
137
+ git push origin "v$VERSION"
138
+
152
139
  - name: Publish to NPM
140
+ if: steps.version-check.outputs.should-publish == 'true'
153
141
  env:
154
142
  NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
155
143
  run: |
156
- # Simple publish - version is already bumped in package.json from PR
144
+ # Version was manually bumped and validated in PR
157
145
  npm publish --access public
158
146
 
159
147
  - name: Create GitHub Release
148
+ if: steps.version-check.outputs.should-publish == 'true'
160
149
  env:
161
150
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
162
151
  run: |
@@ -168,3 +157,9 @@ jobs:
168
157
  --title "Release v$VERSION" \
169
158
  --notes "Automated release of version $VERSION" \
170
159
  --latest
160
+
161
+ - name: Skip publish
162
+ if: steps.version-check.outputs.should-publish == 'false'
163
+ run: |
164
+ echo "ℹ️ No version bump detected - publish skipped"
165
+ echo "This merge completed without triggering a release"
@@ -1,4 +1,8 @@
1
1
  import { configDefaults, defineConfig } from 'vitest/config'
2
+ import dotenv from 'dotenv'
3
+
4
+ // Load environment variables from .env file
5
+ dotenv.config()
2
6
 
3
7
  export default defineConfig({
4
8
  test: {
package/DEVELOPING.md CHANGED
@@ -54,7 +54,7 @@ npm run examples
54
54
  ### Cache Behavior
55
55
  - **Cache Hit**: Returns cached response instantly
56
56
  - **Cache Miss**: Makes LLM API call and caches the response
57
- - **TTL**: Cached responses expire based on `cacheTTL` configuration
57
+ - **TTL**: Cached responses expire after 365 days (configurable via `CHATGPT_CACHE_TTL`)
58
58
  - **Fallback**: Gracefully handles Redis connection failures
59
59
 
60
60
  ## Development Workflow
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@far-world-labs/verblets",
3
- "version": "0.1.4",
4
- "description": "OpenAI Client",
3
+ "version": "0.2.0",
4
+ "description": "Verblets is a collection of tools for building LLM-powered applications.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
7
7
  "repository": {
@@ -15,20 +15,20 @@
15
15
  "--": "npm run script -- generate-verblet foo",
16
16
  "script": "./scripts/run.sh",
17
17
  "test": "vitest",
18
+ "clear-test-cache": "node scripts/clear-redis.js",
18
19
  "examples:warn": "source .env && LLM_EXPECT_MODE=info EXAMPLES=true vitest --config .vitest.config.examples.js",
19
- "examples": "source .env && LLM_EXPECT_MODE=error EXAMPLES=true vitest --config .vitest.config.examples.js",
20
+ "examples": "source .env && LLM_EXPECT_MODE=error EXAMPLES=true ENABLE_LONG_EXAMPLES=false vitest --config .vitest.config.examples.js",
21
+ "examples:all": "source .env && LLM_EXPECT_MODE=error EXAMPLES=true ENABLE_LONG_EXAMPLES=true vitest --config .vitest.config.examples.js",
22
+ "examples:fresh": "npm run clear-test-cache && npm run examples:all",
20
23
  "lint": "eslint 'src/**/*.{js,jsx}'",
21
24
  "lint:fix": "eslint 'src/**/*.{js,jsx}' --fix",
22
25
  "check:deps": "npx npm-deprecated-check current",
23
26
  "husky:install": "husky install",
24
27
  "husky:uninstall": "husky uninstall",
25
28
  "prepare": "npx husky install",
26
- "version:patch": "node scripts/version-bump.js patch",
27
- "version:minor": "node scripts/version-bump.js minor",
28
- "version:major": "node scripts/version-bump.js major",
29
- "release:patch": "node scripts/version-bump.js patch",
30
- "release:minor": "node scripts/version-bump.js minor",
31
- "release:major": "node scripts/version-bump.js major"
29
+ "version:patch": "npm version patch",
30
+ "version:minor": "npm version minor",
31
+ "version:major": "npm version major"
32
32
  },
33
33
  "config": {
34
34
  "gen-script": "./scripts/run.sh gen-$1"
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { getClient as getRedis } from '../src/services/redis/index.js';
4
+
5
+ async function clearRedisKeys() {
6
+ let redis = null;
7
+
8
+ try {
9
+ console.log('🔄 Connecting to Redis...');
10
+ redis = await getRedis();
11
+
12
+ // Check if this is the NullRedisClient (in-memory fallback)
13
+ if (redis.store !== undefined) {
14
+ // This is the in-memory client
15
+ const keyCount = Object.keys(redis.store).length;
16
+ if (keyCount === 0) {
17
+ console.log('✅ In-memory cache is already empty - no keys to clear');
18
+ return;
19
+ }
20
+
21
+ console.log(`🗑️ Found ${keyCount} keys in in-memory cache to clear`);
22
+ redis.store = {};
23
+ console.log(`✅ Successfully cleared ${keyCount} in-memory cache keys`);
24
+ console.log('🧹 In-memory cache has been cleared - tests can now run with fresh responses');
25
+ return;
26
+ }
27
+
28
+ // This is the SafeRedisClient wrapper - access underlying Redis client
29
+ const underlyingClient = redis.redisClient;
30
+ if (!underlyingClient) {
31
+ console.log('⚠️ No underlying Redis client found - using fallback method');
32
+ return;
33
+ }
34
+
35
+ // Get count of keys before clearing
36
+ const keys = await underlyingClient.keys('*');
37
+ const keyCount = keys.length;
38
+
39
+ if (keyCount === 0) {
40
+ console.log('✅ Redis is already empty - no keys to clear');
41
+ return;
42
+ }
43
+
44
+ console.log(`🗑️ Found ${keyCount} keys to clear`);
45
+
46
+ // Use FLUSHDB to clear all keys in the current database
47
+ // This is more efficient and reliable than deleting individual keys
48
+ await underlyingClient.flushDb();
49
+
50
+ console.log(`✅ Successfully cleared all ${keyCount} Redis keys`);
51
+ console.log('🧹 Redis cache has been cleared - tests can now run with fresh responses');
52
+
53
+ } catch (error) {
54
+ if (error.message.includes('ECONNREFUSED') || error.message.includes('connection')) {
55
+ console.log('⚠️ Redis is not running or not accessible - nothing to clear');
56
+ console.log(' This is normal if you\'re using the in-memory cache fallback');
57
+ } else {
58
+ console.error('❌ Error clearing Redis keys:', error.message);
59
+ process.exit(1);
60
+ }
61
+ } finally {
62
+ if (redis && typeof redis.disconnect === 'function') {
63
+ try {
64
+ await redis.disconnect();
65
+ console.log('🔌 Disconnected from Redis');
66
+ } catch (disconnectError) {
67
+ // Ignore disconnect errors
68
+ }
69
+ }
70
+ }
71
+ }
72
+
73
+ // Run the script
74
+ clearRedisKeys();
@@ -0,0 +1,26 @@
1
+ # Conversation
2
+
3
+ A flexible engine that produces multi-speaker transcripts on demand. Provide a list of speakers and conversation rules. The chain manages turn taking, facilitator remarks, and closing summaries.
4
+
5
+ ```javascript
6
+ import Conversation from './src/chains/conversation/index.js';
7
+
8
+ const speakers = [
9
+ { id: 'fac', role: 'facilitator', bio: 'organizes community events' },
10
+ { id: 'max', bio: 'local baker' },
11
+ { id: 'lily', bio: 'youth soccer coach' },
12
+ ];
13
+
14
+ const chain = new Conversation('neighborhood picnic', speakers, {
15
+ rules: { shouldContinue: (round) => round < 2 },
16
+ });
17
+ const transcript = await chain.run();
18
+ console.log(transcript);
19
+ ```
20
+
21
+ Each message in the transcript has the shape `{ id, name, comment, time }` where
22
+ `time` is in `HH:MM` format.
23
+
24
+ The conversation engine uses `conversation-turn-multi` and `conversation-turn` verblets internally to generate contextual responses. You can supply custom `bulkSpeakFn` and `speakFn` implementations for specialized conversation behaviors.
25
+
26
+ Perfect for simulating realistic discussions, focus groups, team meetings, or any multi-participant dialogue where each speaker brings their unique perspective and expertise to the conversation.
@@ -0,0 +1,398 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import ConversationChain from './index.js';
3
+ import { expect as aiExpected } from '../expect/index.js';
4
+ import { longTestTimeout, shouldRunLongExamples } from '../../constants/common.js';
5
+ import { roundRobin } from './turn-policies.js';
6
+
7
+ describe('conversation chain examples', () => {
8
+ it.skipIf(!shouldRunLongExamples)(
9
+ 'generates a debate on consciousness emergence in AI systems - a current open question',
10
+ async () => {
11
+ const speakers = [
12
+ {
13
+ id: 'turing',
14
+ name: 'Alan Turing',
15
+ bio: 'Father of computer science and artificial intelligence, creator of the Turing Test',
16
+ agenda:
17
+ 'Argue that consciousness is fundamentally about behavior and computational processes, not substrate',
18
+ },
19
+ {
20
+ id: 'minsky',
21
+ name: 'Marvin Minsky',
22
+ bio: 'Co-founder of MIT AI Lab, pioneer in artificial intelligence and cognitive science',
23
+ agenda:
24
+ 'Advocate that consciousness emerges from the interaction of simple, unconscious agents in a society of mind',
25
+ },
26
+ {
27
+ id: 'hinton',
28
+ name: 'Geoffrey Hinton',
29
+ bio: 'Godfather of deep learning, pioneer in neural networks and backpropagation',
30
+ agenda:
31
+ 'Argue that consciousness could emerge from sufficiently complex neural networks through self-organizing principles',
32
+ },
33
+ ];
34
+
35
+ const topic =
36
+ 'The Hard Problem of Machine Consciousness: Will AI systems develop genuine subjective experience?';
37
+
38
+ // Hook: Pre-conversation setup
39
+ expect(speakers.length).toBe(3);
40
+ expect(topic.toLowerCase()).toContain('consciousness');
41
+ // BREAKPOINT: Set breakpoint here to inspect speakers and topic
42
+
43
+ const shouldContinueWithHook = (round, _messages) => {
44
+ // Hook: Simple round tracking
45
+ // BREAKPOINT: Set breakpoint here to see round progression and messages
46
+ expect(round).toBeGreaterThanOrEqual(0);
47
+ return round < 3; // 3 rounds to ensure all speakers participate
48
+ };
49
+
50
+ const chain = new ConversationChain(topic, speakers, {
51
+ rules: {
52
+ shouldContinue: shouldContinueWithHook,
53
+ turnPolicy: roundRobin(speakers), // Use deterministic round-robin to ensure all speakers participate
54
+ customPrompt:
55
+ "This is a deep philosophical and scientific discussion. Draw on your expertise and engage with others' arguments. Be intellectually rigorous but concise.",
56
+ },
57
+ });
58
+
59
+ // Hook: Pre-run validation
60
+ expect(chain.speakers.length).toBe(3);
61
+ // BREAKPOINT: Set breakpoint here before conversation starts
62
+
63
+ const messages = await chain.run();
64
+
65
+ // Hook: Post-run analysis
66
+ // BREAKPOINT: Set breakpoint here to examine completed conversation
67
+ expect(messages.length).toBeGreaterThan(2); // At least 3 messages (one per speaker)
68
+
69
+ // Hook: Final participation check
70
+ const speakerIds = new Set(messages.map((m) => m.id));
71
+ expect(speakerIds.size).toBe(3);
72
+ // BREAKPOINT: Set breakpoint here for final analysis
73
+
74
+ // Basic validation
75
+ expect(Array.isArray(messages)).toBe(true);
76
+
77
+ // Hook: Speaker participation analysis
78
+ expect(speakerIds.has('turing')).toBe(true);
79
+ expect(speakerIds.has('minsky')).toBe(true);
80
+ expect(speakerIds.has('hinton')).toBe(true);
81
+
82
+ // Messages should have proper structure
83
+ for (const message of messages) {
84
+ expect(message).toHaveProperty('id');
85
+ expect(message).toHaveProperty('name');
86
+ expect(message).toHaveProperty('comment');
87
+ expect(message).toHaveProperty('time');
88
+ expect(typeof message.comment).toBe('string');
89
+ expect(message.comment.length).toBeGreaterThan(0);
90
+ }
91
+
92
+ // Hook: Content analysis
93
+ const allComments = messages
94
+ .map((m) => m.comment)
95
+ .join(' ')
96
+ .toLowerCase();
97
+ const consciousnessTerms = ['consciousness', 'subjective', 'experience', 'awareness'];
98
+ const foundTerms = consciousnessTerms.filter((term) => allComments.includes(term));
99
+
100
+ expect(foundTerms.length).toBeGreaterThan(0);
101
+
102
+ // AI validation of conversation quality
103
+ const [hasPhilosophicalDepth] = await aiExpected(
104
+ messages,
105
+ undefined,
106
+ 'Should contain sophisticated philosophical discussion about machine consciousness with each pioneer contributing their unique perspective'
107
+ );
108
+
109
+ // Hook: Final validation
110
+ expect(hasPhilosophicalDepth).toBe(true);
111
+ },
112
+ longTestTimeout
113
+ );
114
+
115
+ it.skipIf(!shouldRunLongExamples)(
116
+ 'generates a debate between modern AI researchers with debugging hooks',
117
+ async () => {
118
+ // Hook: Test initialization
119
+
120
+ const speakers = [
121
+ {
122
+ id: 'moderator',
123
+ name: 'AI Debate Moderator',
124
+ bio: 'Expert moderator facilitating discussions on AI research directions. Ask probing questions and guide the conversation.',
125
+ agenda: 'Keep the discussion focused and ensure all perspectives are heard',
126
+ },
127
+ {
128
+ id: 'hinton',
129
+ name: 'Geoffrey Hinton',
130
+ bio: 'Godfather of deep learning, pioneer in neural networks and backpropagation',
131
+ agenda: 'Advocate for deep learning and neural network approaches to AI',
132
+ },
133
+ {
134
+ id: 'li',
135
+ name: 'Fei-Fei Li',
136
+ bio: 'Pioneer in computer vision and AI ethics, former Chief Scientist at Google Cloud',
137
+ agenda: 'Emphasize the importance of visual intelligence and responsible AI development',
138
+ },
139
+ ];
140
+
141
+ const topic =
142
+ 'Deep Learning vs Symbolic AI: Which approach will lead to artificial general intelligence?';
143
+
144
+ // Custom turn policy with hooks
145
+ const moderatedTurnPolicy = (round, _messages) => {
146
+ // Hook: Simple turn policy tracking
147
+ expect(round).toBeGreaterThanOrEqual(0);
148
+
149
+ if (round === 0) {
150
+ return ['moderator', 'hinton', 'li'];
151
+ } else {
152
+ return ['hinton', 'li', 'moderator'];
153
+ }
154
+ };
155
+
156
+ const chain = new ConversationChain(topic, speakers, {
157
+ rules: {
158
+ shouldContinue: (round, _messages) => {
159
+ // Hook: Simple continuation check
160
+ return round < 2; // Only 2 rounds
161
+ },
162
+ turnPolicy: moderatedTurnPolicy,
163
+ },
164
+ });
165
+
166
+ // Hook: Pre-execution state
167
+ expect(chain.speakers.length).toBe(3);
168
+
169
+ const messages = await chain.run();
170
+
171
+ // Hook: Simple results
172
+ expect(Array.isArray(messages)).toBe(true);
173
+ expect(messages.length).toBeGreaterThan(4);
174
+
175
+ // Hook: Moderator participation check
176
+ const moderatorMessages = messages.filter((m) => m.id === 'moderator');
177
+ expect(moderatorMessages.length).toBeGreaterThan(0);
178
+
179
+ // Hook: Researcher participation check
180
+ const researcherIds = ['hinton', 'li'];
181
+ researcherIds.forEach((id) => {
182
+ const count = messages.filter((m) => m.id === id).length;
183
+ expect(count).toBeGreaterThan(0);
184
+ });
185
+
186
+ // AI validation of moderated discussion
187
+ const [hasModeratedDiscussion] = await aiExpected(
188
+ messages,
189
+ undefined,
190
+ 'Should contain a well-moderated discussion with the moderator guiding the conversation and researchers providing technical insights'
191
+ );
192
+
193
+ // Hook: Final assessment
194
+ expect(hasModeratedDiscussion).toBe(true);
195
+ },
196
+ longTestTimeout
197
+ );
198
+
199
+ it.skipIf(!shouldRunLongExamples)(
200
+ 'generates a historical debate between early AI pioneers with custom turn policy',
201
+ async () => {
202
+ const speakers = [
203
+ {
204
+ id: 'turing',
205
+ name: 'Alan Turing',
206
+ bio: 'Father of computer science, proposed the Turing Test in 1950',
207
+ agenda: 'Argue that machines can think and exhibit intelligent behavior',
208
+ },
209
+ {
210
+ id: 'minsky',
211
+ name: 'Marvin Minsky',
212
+ bio: 'Co-founder of MIT AI Lab, expert in cognitive science and AI',
213
+ agenda: 'Discuss the society of mind and modular intelligence',
214
+ },
215
+ {
216
+ id: 'mccarthy',
217
+ name: 'John McCarthy',
218
+ bio: 'Inventor of LISP, advocate for logical AI approaches',
219
+ agenda: 'Promote formal logic and symbolic reasoning in AI',
220
+ },
221
+ ];
222
+
223
+ const topic = 'Can machines truly think, or do they merely simulate thinking?';
224
+
225
+ // Custom turn policy: alternate between Turing and others
226
+ const customTurnPolicy = (round) => {
227
+ if (round === 0) {
228
+ return ['turing', 'minsky', 'mccarthy'];
229
+ } else {
230
+ return ['minsky', 'mccarthy', 'turing'];
231
+ }
232
+ };
233
+
234
+ const chain = new ConversationChain(topic, speakers, {
235
+ rules: {
236
+ shouldContinue: (round) => round < 2, // Only 2 rounds
237
+ turnPolicy: customTurnPolicy,
238
+ },
239
+ });
240
+
241
+ const messages = await chain.run();
242
+
243
+ // Validate turn policy was followed
244
+ expect(messages.length).toBeGreaterThan(4);
245
+
246
+ // All speakers should contribute
247
+ expect(messages.some((m) => m.id === 'turing')).toBe(true);
248
+ expect(messages.some((m) => m.id === 'minsky')).toBe(true);
249
+ expect(messages.some((m) => m.id === 'mccarthy')).toBe(true);
250
+
251
+ // AI validation of philosophical depth
252
+ const [hasPhilosophicalDepth] = await aiExpected(
253
+ messages,
254
+ undefined,
255
+ 'Should contain deep philosophical discussion about machine consciousness and the nature of thinking'
256
+ );
257
+ expect(hasPhilosophicalDepth).toBe(true);
258
+ },
259
+ longTestTimeout
260
+ );
261
+
262
+ it(
263
+ 'handles conversation with custom prompts and includes a summarizer role',
264
+ async () => {
265
+ const speakers = [
266
+ {
267
+ id: 'hinton',
268
+ name: 'Geoffrey Hinton',
269
+ bio: 'Deep learning pioneer, recently left Google to warn about AI risks',
270
+ agenda: 'Discuss both the potential and dangers of advanced AI systems',
271
+ },
272
+ {
273
+ id: 'sutskever',
274
+ name: 'Ilya Sutskever',
275
+ bio: 'OpenAI co-founder, architect of GPT models',
276
+ agenda: 'Focus on the technical path to AGI and alignment challenges',
277
+ },
278
+ {
279
+ id: 'summarizer',
280
+ name: 'Discussion Summarizer',
281
+ bio: 'Expert at synthesizing complex technical discussions. Provide clear summaries of key points.',
282
+ agenda:
283
+ 'Summarize the main arguments and highlight important insights from the discussion',
284
+ },
285
+ ];
286
+
287
+ const topic = 'AI Safety and the Race to AGI: Balancing Progress with Precaution';
288
+
289
+ const customPrompt =
290
+ 'You are participating in a high-stakes debate about AI safety. Be thoughtful, cite specific examples, and acknowledge the complexity of the issues.';
291
+
292
+ // Turn policy: discussion round, then summarizer
293
+ const summaryTurnPolicy = (round) => {
294
+ if (round === 0) {
295
+ return ['hinton', 'sutskever'];
296
+ } else if (round === 1) {
297
+ return ['summarizer'];
298
+ } else {
299
+ return [];
300
+ }
301
+ };
302
+
303
+ const chain = new ConversationChain(topic, speakers, {
304
+ rules: {
305
+ shouldContinue: (round) => round < 2, // Only 2 rounds
306
+ turnPolicy: summaryTurnPolicy,
307
+ customPrompt,
308
+ },
309
+ });
310
+
311
+ const messages = await chain.run();
312
+
313
+ // Validate structure
314
+ expect(messages.length).toBeGreaterThan(2);
315
+ expect(messages.some((m) => m.id === 'hinton')).toBe(true);
316
+ expect(messages.some((m) => m.id === 'sutskever')).toBe(true);
317
+ expect(messages.some((m) => m.id === 'summarizer')).toBe(true);
318
+
319
+ // Summarizer should come last
320
+ const lastMessage = messages[messages.length - 1];
321
+ expect(lastMessage.id).toBe('summarizer');
322
+
323
+ // AI validation for safety-focused discussion with summary
324
+ const [focusesOnSafety] = await aiExpected(
325
+ messages,
326
+ undefined,
327
+ 'Should contain substantive discussion about AI safety, risks, and responsible AGI development, concluding with a clear summary'
328
+ );
329
+ expect(focusesOnSafety).toBe(true);
330
+ },
331
+ longTestTimeout
332
+ );
333
+
334
+ it(
335
+ 'demonstrates flexible speaker ordering and role definitions',
336
+ async () => {
337
+ const speakers = [
338
+ {
339
+ id: 'questioner',
340
+ name: 'Socratic Questioner',
341
+ bio: 'Ask probing questions about AI consciousness and challenge assumptions',
342
+ agenda: 'Use the Socratic method to explore deeper truths about machine intelligence',
343
+ },
344
+ {
345
+ id: 'turing',
346
+ name: 'Alan Turing',
347
+ bio: 'Father of computer science, creator of the Turing Test',
348
+ agenda: 'Defend the possibility of machine consciousness and thinking',
349
+ },
350
+ {
351
+ id: 'skeptic',
352
+ name: 'AI Skeptic',
353
+ bio: 'Philosopher who questions whether machines can truly understand or just manipulate symbols',
354
+ agenda: 'Challenge claims about machine consciousness and understanding',
355
+ },
356
+ ];
357
+
358
+ const topic = 'What does it mean for a machine to truly understand?';
359
+
360
+ // Dynamic turn policy based on conversation flow
361
+ const dynamicTurnPolicy = (round, _history) => {
362
+ if (round === 0) {
363
+ return ['questioner', 'turing', 'skeptic'];
364
+ } else {
365
+ // Final round: questioner gets last word
366
+ return ['turing', 'skeptic', 'questioner'];
367
+ }
368
+ };
369
+
370
+ const chain = new ConversationChain(topic, speakers, {
371
+ rules: {
372
+ shouldContinue: (round) => round < 2, // Only 2 rounds
373
+ turnPolicy: dynamicTurnPolicy,
374
+ },
375
+ });
376
+
377
+ const messages = await chain.run();
378
+
379
+ // Validate all speakers participated
380
+ expect(messages.some((m) => m.id === 'questioner')).toBe(true);
381
+ expect(messages.some((m) => m.id === 'turing')).toBe(true);
382
+ expect(messages.some((m) => m.id === 'skeptic')).toBe(true);
383
+
384
+ // Questioner should have the final word
385
+ const lastMessage = messages[messages.length - 1];
386
+ expect(lastMessage.id).toBe('questioner');
387
+
388
+ // AI validation of Socratic dialogue
389
+ const [hasSocraticDepth] = await aiExpected(
390
+ messages,
391
+ undefined,
392
+ 'Should demonstrate Socratic questioning method with deep philosophical inquiry about machine understanding'
393
+ );
394
+ expect(hasSocraticDepth).toBe(true);
395
+ },
396
+ longTestTimeout
397
+ );
398
+ });