@things-factory/board-import 10.1.154 → 10.1.156

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/board-import",
3
- "version": "10.1.154",
3
+ "version": "10.1.156",
4
4
  "main": "dist-server/index.js",
5
5
  "things-factory": true,
6
6
  "author": "heartyoh",
@@ -23,13 +23,13 @@
23
23
  },
24
24
  "dependencies": {
25
25
  "@operato/graphql": "^10.0.0",
26
- "@things-factory/ai-client-base": "^10.1.149",
27
- "@things-factory/attachment-base": "^10.1.154",
28
- "@things-factory/auth-base": "^10.1.154",
29
- "@things-factory/board-service": "^10.1.154",
26
+ "@things-factory/ai-client-base": "^10.1.155",
27
+ "@things-factory/attachment-base": "^10.1.156",
28
+ "@things-factory/auth-base": "^10.1.155",
29
+ "@things-factory/board-service": "^10.1.156",
30
30
  "@things-factory/env": "^10.1.149",
31
- "@things-factory/shell": "^10.1.154",
31
+ "@things-factory/shell": "^10.1.155",
32
32
  "dxf-parser": "^1.1.2"
33
33
  },
34
- "gitHead": "f28579aa7131cbb282eb8813f8b285d9d29bb525"
34
+ "gitHead": "641146444c8ac4e510854c40824457ffad5cf3f1"
35
35
  }
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * zero-to-twin 메인 트랙 (조감도/사진 업로드 → 보드) 의 첫 어댑터.
9
9
  */
10
- import { getAIClient } from '../ai/types'
10
+ import { getAIClient, type BoardImportAI } from '../ai/types'
11
11
  import type {
12
12
  AdapterCapabilities,
13
13
  AdapterInput,
@@ -21,6 +21,11 @@ import type {
21
21
  const IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.webp', '.gif', '.bmp']
22
22
 
23
23
  export interface ImageParseOptions extends ParseOptions {
24
+ /**
25
+ * The AI of the domain the import runs for - asked only when an image is parsed, so a drawing that needs no AI
26
+ * imports in a domain with no AI agent.
27
+ */
28
+ resolveAI?: () => Promise<BoardImportAI>
24
29
  /** 자연어 컨텍스트 — VLM 에 함께 전달. 예: "factory floor plan with AGV paths and stockers" */
25
30
  context?: string
26
31
  /** 후보 카테고리 — VLM 이 그 안에서 라벨링하도록 가이드 */
@@ -62,9 +67,9 @@ export class ImageAdapter implements FormatAdapter {
62
67
  }
63
68
 
64
69
  async parse(file: AdapterInput, options: ImageParseOptions = {}): Promise<ParseResult> {
65
- const ai = getAIClient()
70
+ const ai = getAIClient() ?? (await options.resolveAI?.())
66
71
  if (!ai) {
67
- throw new Error('Image import requires a registered BoardImportAI (call setAIClient first).')
72
+ throw new Error('Image import needs the AI of the domain it runs for (resolveAI) or a registered BoardImportAI.')
68
73
  }
69
74
  if (!ai.parseFromImage) {
70
75
  throw new Error('Registered BoardImportAI does not implement parseFromImage (no VLM support).')
@@ -7,17 +7,17 @@
7
7
  *
8
8
  * 사용 예:
9
9
  * ```ts
10
- * import { createAIClient } from '@things-factory/ai-client-base'
10
+ * import { aiClientFor } from '@things-factory/ai-client-base'
11
11
  * import { setAIClient, DefaultBoardImportAI } from '@things-factory/board-import'
12
12
  *
13
- * const base = createAIClient({ provider: 'anthropic', apiKey: '...' })
13
+ * const base = await aiClientFor({ domain, user }) // the domain's AI agent
14
14
  * setAIClient(new DefaultBoardImportAI(base))
15
15
  * ```
16
16
  *
17
17
  * Phase 1-2 결정적 파이프라인은 AI 미사용. 위 등록은 Phase 3+ 메서드 활성화용.
18
18
  */
19
19
  import {
20
- getDefaultAIClient,
20
+ aiClientFor,
21
21
  parseJSONResponse,
22
22
  type AIClient as BaseAIClient
23
23
  } from '@things-factory/ai-client-base'
@@ -590,29 +590,22 @@ function hydrateRuleRegexes<T extends { match?: any }>(rule: T): T {
590
590
  // ── Provider 등록 ──────────────────────────────────────────────────
591
591
 
592
592
  let currentClient: BoardImportAI | undefined
593
- let defaultBackedClient: BoardImportAI | undefined
594
- let defaultBackedFor: BaseAIClient | undefined
595
593
 
594
+ /** A BoardImportAI that answers every import, whatever the domain - for tests and for a host that brings its own. */
596
595
  export function setAIClient(client: BoardImportAI | undefined): void {
597
596
  currentClient = client
598
597
  }
599
598
 
599
+ export function getAIClient(): BoardImportAI | undefined {
600
+ return currentClient
601
+ }
602
+
600
603
  /**
601
- * 등록된 BoardImportAI 가 있으면 그걸 반환.
602
- * 없으면 ai-client-base 의 default AIClient (config.aiClient) 를 `DefaultBoardImportAI` 로
603
- * 감싸서 반환 — board-ai 와 동일한 1회 설정만으로 image import 도 동작하게 한다.
604
- *
605
- * default base 가 바뀌면 (테스트 등) 캐시를 무효화하고 다시 감싼다.
604
+ * The BoardImportAI of a domain's AI agent (design/ai/agent-settings.md). An import runs in the background, after the
605
+ * request is gone, so the job asks with the domain and the person it stored.
606
606
  */
607
- export function getAIClient(): BoardImportAI | undefined {
608
- if (currentClient) return currentClient
609
- const base = getDefaultAIClient()
610
- if (!base) return undefined
611
- if (defaultBackedFor !== base) {
612
- defaultBackedFor = base
613
- defaultBackedClient = new DefaultBoardImportAI(base)
614
- }
615
- return defaultBackedClient
607
+ export async function boardImportAIFor(request: { domain: { id: string }; user?: { id: string } | null }): Promise<BoardImportAI> {
608
+ return currentClient ?? new DefaultBoardImportAI(await aiClientFor(request as any))
616
609
  }
617
610
 
618
611
  /** Deprecated alias — 기존 코드 호환용. 새로 작성 시 BoardImportAI 사용. */
@@ -71,6 +71,8 @@ export interface ExtractionContext {
71
71
  * 스케일바). ScaleCalibrator 가 최우선 사용. unitsPerPixel = units / pixels.
72
72
  */
73
73
  scaleReference?: { pixels: number; units: number; unit?: string }
74
+ /** The AI client of the domain the extraction runs for (aiClientFor). A recognizer that asks a model needs it. */
75
+ aiClient?: import('@things-factory/ai-client-base').AIClient
74
76
  [k: string]: any
75
77
  }
76
78
 
@@ -253,8 +253,14 @@ describe('VlmRecognizer', () => {
253
253
  expect(out.confidence).toBeLessThanOrEqual(0.3)
254
254
  })
255
255
 
256
- it('AIClient 없으면 명확히 throw', async () => {
257
- await expect(new VlmRecognizer().recognize(prim())).rejects.toThrow(/AIClient/)
256
+ it('AI client 없으면 명확히 throw — 실행하는 쪽이 그 사업장 에이전트의 client 를 넘긴다', async () => {
257
+ await expect(new VlmRecognizer().recognize(prim())).rejects.toThrow(/needs the AI client of the domain/)
258
+ })
259
+
260
+ it('실행하는 쪽이 넘긴 ctx.aiClient 로 판정한다', async () => {
261
+ const client: any = { generateJSON: async () => ({ componentType: 'rack', confidence: 0.9 }) }
262
+ const r = await new VlmRecognizer().recognize(prim(), { aiClient: client })
263
+ expect(r.componentType).toBe('rack')
258
264
  })
259
265
 
260
266
  it('프롬프트에 region 신호(라벨·크기) + 카탈로그 포함', async () => {
@@ -5,9 +5,10 @@
5
5
  * *의미 인식* 만: 주어진 신호(라벨·모양·크기·레이어) + 카탈로그 → componentType.
6
6
  * 카탈로그(ctx.catalog) 가 있으면 그 안의 값만 허용(그라운딩).
7
7
  *
8
- * provider 는 ai-client-base 의 AIClient. 생성자 주입 가능(테스트), 미주입 시 기본 클라이언트.
8
+ * provider 는 ai-client-base 의 AIClient. 생성자로 주입하거나(테스트), 실행하는 쪽이 그 사업장 에이전트의 client 를
9
+ * ctx.aiClient 로 넘긴다.
9
10
  */
10
- import { getDefaultAIClient, type AIClient } from '@things-factory/ai-client-base'
11
+ import type { AIClient } from '@things-factory/ai-client-base'
11
12
  import type { Recognizer, RecognitionResult, GeometricPrimitive, ExtractionContext } from './contracts'
12
13
 
13
14
  interface RawRecognition {
@@ -27,9 +28,9 @@ export class VlmRecognizer implements Recognizer {
27
28
  constructor(private readonly client?: AIClient) {}
28
29
 
29
30
  async recognize(region: GeometricPrimitive, ctx?: ExtractionContext): Promise<RecognitionResult> {
30
- const client = this.client ?? getDefaultAIClient()
31
+ const client = this.client ?? ctx?.aiClient
31
32
  if (!client) {
32
- throw new Error('[board-import/extraction] VlmRecognizer 는 AIClient 가 필요합니다 (setDefaultAIClient / config.aiClient).')
33
+ throw new Error('[board-import/extraction] VlmRecognizer needs the AI client of the domain it runs for (ctx.aiClient).')
33
34
  }
34
35
  const raw = await client.generateJSON<RawRecognition>(buildPrompt(region, ctx), {
35
36
  systemPrompt: SYSTEM,
@@ -127,6 +127,9 @@ export class ImportSession {
127
127
  @Field(type => User, { nullable: true })
128
128
  creator?: User
129
129
 
130
+ @RelationId((session: ImportSession) => session.creator)
131
+ creatorId?: string
132
+
130
133
  @CreateDateColumn()
131
134
  @Field({ nullable: true })
132
135
  createdAt?: Date
@@ -16,6 +16,7 @@ import { Attachment } from '@things-factory/attachment-base'
16
16
  import { STORAGE } from '@things-factory/attachment-base'
17
17
 
18
18
  import { importBoardVariants } from '../pipeline/index'
19
+ import { boardImportAIFor } from '../ai/types'
19
20
  import { ImportSession } from './import-session'
20
21
 
21
22
  export async function runImportJob(sessionId: string): Promise<void> {
@@ -50,7 +51,9 @@ export async function runImportJob(sessionId: string): Promise<void> {
50
51
  // 다른 adapter (DXF) 에는 무해 (몰라도 무시).
51
52
  const mergedParseOptions = {
52
53
  ...(opts.parseOptions || {}),
53
- ...(opts.userPrompt ? { userPrompt: opts.userPrompt } : {})
54
+ ...(opts.userPrompt ? { userPrompt: opts.userPrompt } : {}),
55
+ /* The domain's AI agent, asked only if the drawing needs it (design/ai/agent-settings.md). */
56
+ resolveAI: () => boardImportAIFor({ domain: { id: session.domainId! }, user: session.creatorId ? { id: session.creatorId } : null })
54
57
  }
55
58
  // AI 가 같은 도면을 다른 관점으로 해석한 multi-variant. parse 한 번, 시안별 mapping + assemble.
56
59
  // DXF 등 single-variant adapter 는 1 boardModel 만.