@things-factory/ai-assistant 10.1.241 → 10.1.245

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/ai-assistant",
3
- "version": "10.1.241",
3
+ "version": "10.1.245",
4
4
  "main": "dist-server/index.js",
5
5
  "things-factory": true,
6
6
  "author": "heartyoh",
@@ -35,13 +35,14 @@
35
35
  "@operato/shell": "^10.29.39",
36
36
  "@operato/styles": "^10.0.0",
37
37
  "@operato/utils": "^10.29.39",
38
- "@things-factory/ai-client-base": "^10.1.241",
39
- "@things-factory/apptool-base": "^10.1.234",
40
- "@things-factory/attachment-base": "^10.1.241",
41
- "@things-factory/auth-base": "^10.1.241",
38
+ "@things-factory/ai-client-base": "^10.1.245",
39
+ "@things-factory/apptool-base": "^10.1.245",
40
+ "@things-factory/attachment-base": "^10.1.245",
41
+ "@things-factory/auth-base": "^10.1.245",
42
42
  "@things-factory/env": "^10.1.149",
43
- "@things-factory/setting-base": "^10.1.241",
44
- "@things-factory/shell": "^10.1.234",
43
+ "@things-factory/help": "^10.1.245",
44
+ "@things-factory/setting-base": "^10.1.245",
45
+ "@things-factory/shell": "^10.1.245",
45
46
  "dompurify": "^3.0.0",
46
47
  "exceljs": "4.2.0",
47
48
  "graphql-scalars": "^1.22.0",
@@ -57,5 +58,5 @@
57
58
  "copyfiles": "^2.4.1",
58
59
  "rimraf": "^5.0.0"
59
60
  },
60
- "gitHead": "d7fe2e1a1eb7c02cb21b897da26d385df014f908"
61
+ "gitHead": "f7479631094a1c70b486485b556885b069025f88"
61
62
  }
@@ -1,6 +1,7 @@
1
1
  /* Read by the resolver and by the boot report, so the report and the conversation open the same tools. */
2
2
  import { entryTargetOf } from '../data-entry/entry-target.js'
3
3
  import { detailReadOf, readTargetOf } from '../data-read/read-target.js'
4
+ import { HELP_CATEGORY } from '../help-documents/help-keys.js'
4
5
  import { PEOPLE_CATEGORIES } from '../people/people-keys.js'
5
6
  /**
6
7
  * A category another package opens in every conversation - a dashboard card (ADR-0160 decision 9). It is in reach of
@@ -53,6 +54,8 @@ export function modeToolCategories(mode: { toolCategories?: string[]; attachment
53
54
  'assistant-workspace',
54
55
  /* Every conversation may read who works here and offer to bring one of them in (people/, ADR-0073 decision 3). */
55
56
  ...PEOPLE_CATEGORIES,
57
+ /* Every conversation may read the application's help (ADR-0181). */
58
+ HELP_CATEGORY,
56
59
  ...(mode.attachments?.kinds?.length ? ['chat-attachments'] : []),
57
60
  ...(mode.entryTargets ?? []).map(key => `data-entry:${key}`),
58
61
  ...(mode.readTargets ?? []).map(key => `data-read:${key}`),
@@ -143,6 +146,8 @@ export function modeFrontCategories(mode: FrontMode, screen: TurnScreen = {}): s
143
146
  * there was no tool to look people up (2026-10-24).
144
147
  */
145
148
  ...PEOPLE_CATEGORIES,
149
+ /* Two tools, always in front: 「지출결의는 어디서 올려?」 is asked on any screen (ADR-0181). */
150
+ HELP_CATEGORY,
146
151
  ...(mode.attachments?.kinds?.length ? ['chat-attachments'] : []),
147
152
  ...keys.filter(key => (mode.readTargets ?? []).includes(key)).map(key => `data-read:${key}`),
148
153
  ...keys.filter(key => (mode.entryTargets ?? []).includes(key)).map(key => `data-entry:${key}`),
@@ -33,6 +33,7 @@ describe('the open screen decides what goes in front', () => {
33
33
  'assistant-workspace',
34
34
  'data-read:organization.employee',
35
35
  'chat-invite',
36
+ 'help-documents',
36
37
  'chat-attachments',
37
38
  'data-read:office.cash-transaction',
38
39
  'data-entry:office.cash-transaction'
@@ -69,12 +70,12 @@ describe('the open screen decides what goes in front', () => {
69
70
  })
70
71
 
71
72
  it('★ a record the mode does not read or enter is not put in front', () => {
72
- expect(modeFrontCategories({ ...mode, readTargets: [], entryTargets: [] }, { page: 'cash-transaction-list' })).toEqual(['office-ops', 'assistant-workspace', 'data-read:organization.employee', 'chat-invite', 'chat-attachments'])
73
+ expect(modeFrontCategories({ ...mode, readTargets: [], entryTargets: [] }, { page: 'cash-transaction-list' })).toEqual(['office-ops', 'assistant-workspace', 'data-read:organization.employee', 'chat-invite', 'help-documents', 'chat-attachments'])
73
74
  })
74
75
 
75
76
  it('no screen: the mode\'s own, the AI 작업 패널 and the attachment reader', () => {
76
- expect(modeFrontCategories(mode)).toEqual(['office-ops', 'assistant-workspace', 'data-read:organization.employee', 'chat-invite', 'chat-attachments'])
77
- expect(modeFrontCategories(mode, { page: 'some-other-page' })).toEqual(['office-ops', 'assistant-workspace', 'data-read:organization.employee', 'chat-invite', 'chat-attachments'])
77
+ expect(modeFrontCategories(mode)).toEqual(['office-ops', 'assistant-workspace', 'data-read:organization.employee', 'chat-invite', 'help-documents', 'chat-attachments'])
78
+ expect(modeFrontCategories(mode, { page: 'some-other-page' })).toEqual(['office-ops', 'assistant-workspace', 'data-read:organization.employee', 'chat-invite', 'help-documents', 'chat-attachments'])
78
79
  })
79
80
 
80
81
  it('the screens the boot report walks: none, each list page, each axis', () => {
@@ -36,7 +36,7 @@ it('★ the settings come from the registered mode', () => {
36
36
  /* The app's persona, then the rules every conversation follows (design/ai/screen-pattern.md §4). */
37
37
  systemPrompt: conversationPrompt('you answer about one space'),
38
38
  /* Every conversation may put a large result on the AI 작업 패널 (design/ai/workspace.md). */
39
- toolCategories: ['twin-ops', 'assistant-workspace', 'data-read:organization.employee', 'chat-invite'],
39
+ toolCategories: ['twin-ops', 'assistant-workspace', 'data-read:organization.employee', 'chat-invite', 'help-documents'],
40
40
  requireToolOnFirstTurn: true,
41
41
  temperature: 0.2,
42
42
  maxTokens: 2048
@@ -68,8 +68,8 @@ describe('declaring a target', () => {
68
68
  })
69
69
 
70
70
  it('a mode opens a target by naming it', () => {
71
- expect(modeToolCategories({ toolCategories: ['office-ops'], entryTargets: ['office.expense-line'] })).toEqual(['office-ops', 'assistant-workspace', 'data-read:organization.employee', 'chat-invite', 'data-entry:office.expense-line'])
72
- expect(modeToolCategories({ toolCategories: ['office-ops'] })).toEqual(['office-ops', 'assistant-workspace', 'data-read:organization.employee', 'chat-invite'])
71
+ expect(modeToolCategories({ toolCategories: ['office-ops'], entryTargets: ['office.expense-line'] })).toEqual(['office-ops', 'assistant-workspace', 'data-read:organization.employee', 'chat-invite', 'help-documents', 'data-entry:office.expense-line'])
72
+ expect(modeToolCategories({ toolCategories: ['office-ops'] })).toEqual(['office-ops', 'assistant-workspace', 'data-read:organization.employee', 'chat-invite', 'help-documents'])
73
73
  })
74
74
  })
75
75
 
@@ -0,0 +1,2 @@
1
+ /* The help tools' category - every conversation carries it (mode-categories.ts), so it is named apart from the tools. */
2
+ export const HELP_CATEGORY = 'help-documents'
@@ -0,0 +1,40 @@
1
+ /*
2
+ * The help tools (ADR-0181): the assistant finds a part of the application's help and reads it, in the person's language.
3
+ */
4
+ jest.mock('@things-factory/help', () => {
5
+ const { HelpIndex, partsOfDocument } = jest.requireActual('../../../../help/server/help-index')
6
+ const index = new HelpIndex([
7
+ ...partsOfDocument('office/guide-employee.ko.html', '<h1>모든 직원</h1><h2 id="spending">경비 · 지출 올리기</h2><p>내 정보 › 내 지출결의 에서 새 결의를 작성합니다.</p>'),
8
+ ...partsOfDocument('office/guide-employee.html', '<h1>Everyone</h1><h2 id="spending">Spending</h2><p>Write a new claim in My expense claims.</p>')
9
+ ])
10
+ return { searchHelp: (q: string, o: any) => index.search(q, o), readHelp: (t: string, s: string, l: string) => index.read(t, s, l) }
11
+ })
12
+
13
+ import { helpCategory, readHelpDocument, searchHelpDocuments } from './help-tools'
14
+ import { modeFrontCategories, modeToolCategories } from '../assistant-mode/mode-categories'
15
+
16
+ const korean = { state: { lng: 'ko' } }
17
+
18
+ describe('the help tools', () => {
19
+ it('finds a part with its title and its link', () => {
20
+ expect(searchHelpDocuments({ query: '지출결의 올리기' }, korean)).toEqual({
21
+ found: 1,
22
+ parts: [{ topic: 'office/guide-employee', section: 'spending', title: '모든 직원 › 경비 · 지출 올리기', summary: '내 정보 › 내 지출결의 에서 새 결의를 작성합니다.', link: 'help?topic=office%2Fguide-employee&section=spending' }]
23
+ })
24
+ })
25
+
26
+ it('says when the help has nothing, so the model does not answer from elsewhere', () => {
27
+ expect(searchHelpDocuments({ query: '주식 매수 방법' }, korean)).toMatchObject({ found: 0, message: expect.stringContaining('say so') })
28
+ })
29
+
30
+ it('reads a part in the person\'s language', () => {
31
+ expect(readHelpDocument({ topic: 'office/guide-employee', section: 'spending' }, { state: { lng: 'en' } })).toMatchObject({ found: true, title: 'Spending' })
32
+ expect(readHelpDocument({ topic: 'office/guide-employee', section: 'none' }, korean)).toMatchObject({ found: false })
33
+ })
34
+
35
+ it('reaches no door, and every conversation has it in front', () => {
36
+ expect(helpCategory.specs.map(spec => spec.doors)).toEqual([[], []])
37
+ expect(modeToolCategories({})).toContain('help-documents')
38
+ expect(modeFrontCategories({})).toContain('help-documents')
39
+ })
40
+ })
@@ -0,0 +1,86 @@
1
+ /*
2
+ * The application's help documents, for the assistant (ADR-0181) - the same index the screen's `?` asks
3
+ * (@things-factory/help). How to do a piece of work, which screen it is on, what a word means: the model finds the part
4
+ * and reads it, rather than the mode's prompt carrying the manual.
5
+ *
6
+ * The help is no one's secret - every screen's `?` opens it for whoever is there - so the tools reach no door. Whether
7
+ * the person may open the screen a part names is still that screen's own door.
8
+ */
9
+ import { registerToolCategory, type ToolCategory, type ToolSpec } from '@things-factory/ai-client-base'
10
+ import { readHelp, searchHelp } from '@things-factory/help'
11
+
12
+ import { HELP_CATEGORY } from './help-keys.js'
13
+
14
+ /** Where a part opens in the application - the help page at that part. */
15
+ export const helpLink = (topic: string, section = '') => `help?topic=${encodeURIComponent(topic)}${section ? `&section=${encodeURIComponent(section)}` : ''}`
16
+
17
+ const languageOf = (context: any): string | undefined => context?.state?.lng || context?.state?.language || undefined
18
+
19
+ export function searchHelpDocuments(args: { query?: string; limit?: number }, context?: any) {
20
+ const query = String(args?.query ?? '').trim()
21
+ if (!query) return { found: 0, parts: [], message: 'name what to look for.' }
22
+ const parts = searchHelp(query, { lang: languageOf(context), limit: Math.min(Math.max(Number(args?.limit) || 5, 1), 10) }).map(hit => ({
23
+ topic: hit.topic,
24
+ section: hit.section,
25
+ title: hit.documentTitle === hit.title ? hit.title : `${hit.documentTitle} › ${hit.title}`,
26
+ summary: hit.summary,
27
+ link: helpLink(hit.topic, hit.section)
28
+ }))
29
+ return parts.length ? { found: parts.length, parts } : { found: 0, parts: [], message: 'the help has nothing on this - say so; do not answer from general knowledge as if it were this application.' }
30
+ }
31
+
32
+ export function readHelpDocument(args: { topic?: string; section?: string }, context?: any) {
33
+ const topic = String(args?.topic ?? '').trim()
34
+ const section = String(args?.section ?? '').trim()
35
+ const part = topic ? readHelp(topic, section, languageOf(context)) : null
36
+ if (!part) return { found: false, message: `no help part ${topic}${section ? `#${section}` : ''} - search again with searchHelp.` }
37
+ return { found: true, ...part, link: helpLink(part.topic, part.section) }
38
+ }
39
+
40
+ const searchSpec = {
41
+ kind: 'read',
42
+ name: 'searchHelp',
43
+ doors: [],
44
+ description:
45
+ "Search this application's help and manuals (how to do a piece of work, which screen and menu it is on, what a term means). " +
46
+ 'Returns the matching parts with a one-line summary and a link; read one with readHelp.',
47
+ schema: {
48
+ type: 'object',
49
+ properties: {
50
+ query: { type: 'string', description: 'The words to look for, in the language of the help - a screen or menu name, a task, a term ("지출결의 지급", "휴가 신청").' },
51
+ limit: { type: 'number', description: 'How many parts, 1-10. Default 5.' }
52
+ },
53
+ required: ['query']
54
+ },
55
+ builder: (args: any, context: any) => searchHelpDocuments(args, context)
56
+ } as ToolSpec
57
+
58
+ const readSpec = {
59
+ kind: 'read',
60
+ name: 'readHelp',
61
+ doors: [],
62
+ description: 'Read one part of the help (topic and section from searchHelp), or the whole document when section is left out.',
63
+ schema: {
64
+ type: 'object',
65
+ properties: {
66
+ topic: { type: 'string', description: 'The topic from searchHelp ("office/guide-employee").' },
67
+ section: { type: 'string', description: 'The section from searchHelp ("leave"). Leave out for the whole document.' }
68
+ },
69
+ required: ['topic']
70
+ },
71
+ builder: (args: any, context: any) => readHelpDocument(args, context)
72
+ } as ToolSpec
73
+
74
+ export const helpCategory: ToolCategory = {
75
+ name: HELP_CATEGORY,
76
+ description: "the application's help and manuals",
77
+ guidance: [
78
+ '- When the person asks how to do something in this application, where a screen or menu is, what the steps are, or what a term on a screen means, call searchHelp, then readHelp on the part that answers it - before you answer.',
79
+ '- Answer from what readHelp returned, in your own short words, and end with the part as a link: [its title](its link). Never invent a menu, a button or a step the help does not name.',
80
+ '- When searchHelp finds nothing, say the help does not cover it; then answer only what your other tools show.',
81
+ '- The help says how the application works; figures and records still come from the other tools.'
82
+ ].join('\n'),
83
+ specs: [searchSpec, readSpec]
84
+ }
85
+
86
+ registerToolCategory(helpCategory)
@@ -14,6 +14,7 @@ import './chat-session/session-purge.js'
14
14
  import './chat-session/attachment-route.js'
15
15
  import { EntryProposalResolver } from './data-entry/entry-resolver.js'
16
16
  import './workspace/workspace-tools.js'
17
+ import './help-documents/help-tools.js'
17
18
  /* Who works here, and the card that brings one of them into a conversation - every conversation carries both. */
18
19
  import './people/index.js'
19
20
  import { entities as UnattendedRunEntities } from './unattended-run/index.js'