AI-Agent ↔ DeviceAgent 인터페이스
구현 협의용 최신 문서: DeviceAgent ↔ AI-Agent 구현 요청서. 아래는 이전 상세·Sample 자료다. 직접 구현 책임과 현재 취소·응답 대기 기준은 신규 요청서를 먼저 확인한다.
연동 보강: 등록·서버 저장 완료 판정, 실패·늦은 응답·복구, 프로필 삭제·동의 철회, 시험별 기대 결과. 표의 제품 제안은 구현 전 규칙이며 기존 Bundle에 미구현 필드를 추가하라는 의미가 아니다.
서버 관심사 조회 (09-17 추가)
| 송신 → 수신 | method | 핵심 값 |
|---|---|---|
| AI-Agent → DeviceAgent → IoTAgent | getVoicePersonalization | voice-personalization-v1, requestId, voiceId, issuedAtMs, expiresAtMs |
| IoTAgent → DeviceAgent → AI-Agent | voicePersonalizationResult | 동일 requestId/voiceId, success, reasonCode, personalizationEnabled, interestCodes, revision, expiresAtMs |
필수 타입·유효기간·실패·JSON·구조도 전체 명세. 원본은 서버, 기기 결과 보관은 5초 요청 기한까지다. 아래 이전 set/getVoiceProfileInterests는 더 이상 DB를 처리하지 않고 SERVER_OWNED_PERSONALIZATION을 반환한다.
DeviceAgent가 AI-Agent로 보내는 실행 요청, AI-Agent가 돌려주는 상태·관리 결과, 프로필 전달과 그 ACK를 구분한다. 등록 App용 화면 실행 Intent와는 다른 경계다.
DeviceAgent 중심 구조
SVG 구조 그림 크게 보기 · 등록 App·AI-Agent ↔ DeviceAgent 전체 인터페이스 정의
등록 App은 화면과 사용자 입력을 담당하고, 등록·인식을 수행하는 서비스는 AI-Agent다. 아래 그림은 그중 등록 과정의 중계 순서다. DeviceAgent가 직접 녹음하거나 화자를 식별하는 구조는 아니다.
getVoicePersonalization
방향: AI-Agent → DeviceAgent → IoTAgent → Backend. AI-Agent가 sendModuleCommand로 요청하고 DeviceAgent는 CALLER_IOT=2 callback에 전달한다. 등록 App은 이 요청을 보내지 않는다.
| 필드 | Bundle 타입 | 필수·값 |
|---|---|---|
method |
String | getVoicePersonalization |
contractVersion |
String | voice-personalization-v1 |
requestId |
String | 새 요청 UUID. 검증: [A-Za-z0-9_-]{1,128} |
voiceId |
String | 인식된 등록 화자 ID. 검증: [A-Za-z0-9_-]{1,128} |
issuedAtMs |
Long | 생성 시각, Unix ms. 미래 시각 거절 |
expiresAtMs |
Long | AI-Agent 생성 시각 + 5,000ms. DeviceAgent는 만료 또는 요청 기간 10,000ms 초과 거절 |
DeviceAgent는 AI-Agent 호출자와 버전·타입·기한을 검사한다. 유효 요청은 0, 거절은 -1이다. 0은 콜백 중계 수락이며 서버 조회 완료가 아니다. MQTT 연결이 없으면 별도 실패 콜백 없이 기한이 끝날 수 있다.
호출 조건·처리: 등록 화자 확정과 관심사 기능 활성 후 새 요청 한 건을 추적한다. 새 요청은 이전 pending과 임시 결과를 비운다.
오류·중복·검증: 잘못된 caller·타입·만료는 거절하며 별도 callback이 없을 수 있다. 다른 requestId/voiceId, 취소 후 응답, 중복 응답을 사용하지 않는지 확인한다.
{
"method": "getVoicePersonalization",
"contractVersion": "voice-personalization-v1",
"requestId": "query-001",
"voiceId": "voice-001",
"issuedAtMs": 1789689600000,
"expiresAtMs": 1789689605000
}
시각은 형식 예시다. 실행 시 현재 시각과 5초 후 시각으로 대체한다.
voicePersonalizationResult
방향: Backend → IoTAgent → DeviceAgent → AI-Agent. IoTAgent가 sendModuleCommand로 보내고 DeviceAgent는 CALLER_LLM=1 callback으로 전달한다. 등록 App에는 전달하지 않는다.
| 필드 | Bundle 타입 | 필수·값 |
|---|---|---|
method |
String | voicePersonalizationResult |
contractVersion |
String | voice-personalization-v1 |
requestId |
String | 원 요청과 동일 |
voiceId |
String | 원 요청과 동일 |
success |
Boolean | 항상 포함 |
reasonCode |
String | 항상 포함. 성공은 빈 문자열 |
expiresAtMs |
Long | 원 요청 기한 유지; 서버에서 연장하지 않음 |
personalizationEnabled |
Boolean | 성공 시 필수 |
interestCodes |
ArrayList<String> | 성공 시 필수. 개인화 비허용이면 [] |
revision |
Long | 성공 시 필수, 1 이상 |
허용 코드는 WEATHER, AIR_QUALITY, HEALTH, SCHEDULE, TRAVEL, FOOD, CULTURE, SPORTS, FINANCE, TECHNOLOGY다. 최대 10개, 중복 금지이며 빈 목록은 관심사 미설정이다. putStringArrayList를 사용한다.
서버 실패는 PROFILE_NOT_FOUND 또는 BACKEND_UNAVAILABLE이며 interestCodes를 포함하지 않는다. DeviceAgent는 IoTAgent 호출자·타입·기한을 검사하고, AI-Agent는 현재 requestId·voiceId·기한을 대조한다. 종료된 요청·다른 화자·중복 응답을 사용하지 않는다. 조회 결과는 원 요청의 5초 기한까지만 임시 보관하고 로컬 DB에 저장하지 않는다.
현재 서버 통신은 VoiceProfileMqttTestBridge와 샘플 Backend 경로다. 제품 AWS 인증·권한·통신 검증과 LLM 프롬프트 자동 주입은 완료 범위가 아니다.
호출 조건·처리: 서버가 원 요청에 대응해 반환한다. AI-Agent는 유효 성공만 snapshot에 보관한다.
오류·중복·검증: 실패에는 interestCodes를 넣지 않는다. 동일 결과 재수신·기한 연장·다른 화자 응답이 무시되는지 확인한다.
{
"method": "voicePersonalizationResult",
"contractVersion": "voice-personalization-v1",
"requestId": "query-001",
"voiceId": "voice-001",
"success": true,
"reasonCode": "",
"expiresAtMs": 1789689605000,
"personalizationEnabled": true,
"interestCodes": [
"WEATHER",
"HEALTH"
],
"revision": 2
}
1. 방향과 함수
AI-Agent 내부 등록·인식·저장 구조와 알고리즘은 별도 설계 페이지에서 확인한다.
| 방향 | 실제 함수 | 내용 |
|---|---|---|
| DeviceAgent → AI-Agent | onModuleCallbackLoop(CALLER_LLM=1, Bundle) → AI-Agent callback | 등록 제어, 관리 요청, voiceProfileCommitResult, voicePersonalizationResult |
| AI-Agent → DeviceAgent | sendModuleCommand(input, output) | voiceProfileStateChanged, commitVoiceProfile, 관리 결과, getVoicePersonalization |
DeviceAgent는 등록 상태·관리 결과를 만들어내지 않는다. commitVoiceProfile 수신 후 대기 저장 결과만 DeviceAgent가 직접 생성한다. sendModuleCommand의 Int 반환값과 callback의 업무 결과를 구분한다.
2. DeviceAgent → AI-Agent 실행 요청
공통: method:String, contractVersion:String, requestId:String. issuedAtMs/expiresAtMs가 있으면 Long 타입을 보존한다. 기존 등록·관리 경로의 Service 만료 검사는 보장되지 않는다. 신규 서버 관심사 조회는 별도 버전과 기한 검사를 적용한다.
등록 제어 9종은 등록 App 송신 표에 정의한 Bundle을 그대로 중계한다. AI-Agent는 녹음·재녹음·취소·개인화 처리를 수행한다. IoTAgent의 원격 start는 먼저 등록 Activity를 열므로 바로 이 실행 요청과 같지 않다.
| 관리 요청 method | 버전 | 추가 입력 키 : 타입 | AI-Agent가 보내는 결과 method |
|---|---|---|---|
| listVoiceProfiles | voice-profile-v2 | 없음 | voiceProfileListResult |
| deleteVoiceProfile | voice-profile-v2 | voiceId:String | voiceProfileDeleteResult |
limit은 기본 20, 1~50이며 beforeTurnId는 기본 0, 0 이상이다. voiceId는 1~128자 영문·숫자·하이픈·밑줄이다. 관심사 변경은 모바일 → Backend HTTPS 경로이며 이 관리 요청 표에 포함하지 않는다. 구형 setVoiceProfileInterests/getVoiceProfileInterests가 AI-Agent까지 도달하면 voiceProfileInterestsResult(success=false, reasonCode=SERVER_OWNED_PERSONALIZATION)를 반환한다. 이 실패에는 voiceId와 interestCodes가 없다.
3. AI-Agent → DeviceAgent 상태 이벤트
method=voiceProfileStateChanged, contractVersion=voice-profile-v2.
| 키 | 타입 | 필수·조건 | 의미 |
|---|---|---|---|
| requestId | String | 필수 | 현재 등록 요청 |
| stateSequence | Long | 필수 | 상태 순번 |
| state | String | 필수 | Service enum 이름 |
| setIndex | Int | 필수 | 현재 세트 |
| setCount | Int | 필수 | 전체 세트 수 |
| progress | Int | 필수 | 진행률 0~100 |
| reasonCode | String | 필수 | 오류/대기 사유, 빈 문자열 허용 |
| terminal | Boolean | 필수 | 종료 여부 |
| nickname | String | 필수 | 표시 이름, 초기 상태는 빈 문자열 가능 |
| voiceId | String | 생성 후 | 생성된 화자 키 |
DeviceAgent는 등록 App(8)과 IoTAgent(2)로 동일 상태를 복사 전달한다. success 필드를 만들어 성공/실패로 바꾸지 않는다. 상태 enum·화면 대응은 등록 App 수신 규칙을 함께 본다.
4. AI-Agent → DeviceAgent 프로필 전달
이 단계에서 AI-Agent는 이미 로컬 프로필을 생성했다. DeviceAgent는 전달 대기 저장을 수행한다. 등록 App은 임베딩을 받지 않는다.
| Bundle 키 | 타입 | 송신 조건 | 의미 |
|---|---|---|---|
| method | String | 필수 | commitVoiceProfile |
| contractVersion | String | 필수 | voice-profile-v2 |
| requestId | String | 필수 | 원래 등록 요청 ID |
| voiceId | String | 필수 | AI-Agent가 생성한 화자 키 |
| nickname | String | 필수 | 사용자 표시 이름 |
| locale | String | 전송 | 언어 |
| personalizationEnabled | Boolean | 전송 | 최종 개인화 선택 |
| referenceEmbedding | FloatArray | 필수 | 대표 임베딩. putFloatArray 사용 |
DeviceAgent 검증 상한: voiceId 1~128자, 임베딩 1~4096개, 각 값은 유한한 수. 4096은 실제 모델 차원이 아니라 방어 상한이다. 모델별 벡터 차원은 생성 모델을 따른다. 원본 audio/pcm/wav는 금지다.
송신 코드 예제
val input = Bundle().apply {
putString("method", "commitVoiceProfile")
putString("contractVersion", "voice-profile-v2")
putString("requestId", requestId)
putString("voiceId", voiceId)
putString("nickname", nickname)
putString("locale", locale)
putBoolean("personalizationEnabled", personalizationEnabled)
putFloatArray("referenceEmbedding", referenceEmbedding)
}
DeviceControlProxy.getService()?.sendModuleCommand(input, Bundle())
변수는 실제 등록 결과다. 임베딩을 JSON 문자열·ByteArray·임의 숫자 배열로 바꾸지 않는다. 코드에서는 각 모듈의 Contract 상수를 사용한다.
5. DeviceAgent → AI-Agent 저장 결과
DeviceAgent가 검증·암호화 대기 저장 후 다음 Bundle을 CALLER_LLM=1 callback으로 반환한다.
| Bundle 키 | 타입 | 포함 | 값·의미 |
|---|---|---|---|
| method | String | 항상 | voiceProfileCommitResult |
| contractVersion | String | 항상 | voice-profile-v2 |
| requestId | String | 항상 | commit의 requestId |
| accepted | Boolean | 항상 | DeviceAgent 대기 저장 성공 |
| cloudSynced | Boolean | 항상 | 제품 Cloud 동기화 여부 또는 로컬 시험 ACK 값 |
| reasonCode | String | 항상 | 아래 결과 사유 |
현재 ACK에는 voiceId가 없다. AI-Agent는 동일 requestId의 pending 프로필과 연결한다. 상대 구현에서 임의로 voiceId를 필수 수신값으로 추가하지 않는다.
| accepted | cloudSynced | reasonCode | 현재 의미 |
|---|---|---|---|
| false | false | INVALID_PROFILE_PAYLOAD | 검증 실패 |
| false | false | DEVICE_OUTBOX_FAILED | 대기 저장 실패 |
| true | false | WAITING_CLOUD_SYNC | 로컬 대기 저장 성공, Cloud 미연결. Service는 DELIVERY_FAILED |
| true | true | LOCAL_TEST_ACK | 시험 프로퍼티로 완료 처리. 실제 Cloud 저장 증거 아님 |
{
"method": "voiceProfileCommitResult",
"contractVersion": "voice-profile-v2",
"requestId": "vp-example-001",
"accepted": true,
"cloudSynced": false,
"reasonCode": "WAITING_CLOUD_SYNC"
}
JSON은 Bundle 값 설명용이다. 현재 제품 Cloud ACK 경로·늦은 ACK 처리 정책은 미구현이며 성공값을 강제로 돌려주면 안 된다. commit의 Int 반환은 accepted이면 0, 저장/검증 실패이면 -1이며 별도 callback도 발생할 수 있다. 호출자 거절은 callback 전에 반환될 수 있다.
6. AI-Agent → DeviceAgent 관리 결과
공통 키: method:String, contractVersion:String, requestId:String, success:Boolean, reasonCode:String. 요청 ID/버전은 대응하는 요청과 맞춘다. DeviceAgent는 결과 검증 후 IoTAgent(2)로 전달하며 등록 App으로 전달하지 않는다.
| 결과 method | 성공 시 추가 Bundle 키 : 타입 |
|---|---|
| voiceProfileListResult | profiles:ArrayList<Bundle> |
| voiceProfileDeleteResult | voiceId:String |
| 중첩 위치 | 키 | 타입 | 의미 |
|---|---|---|---|
| profiles[] | voiceId | String | 등록 화자 키 |
| profiles[] | nickname | String | 표시 이름 |
| profiles[] | personalizationEnabled | Boolean | 프로필에 기록된 개인화 동의. 예전 프로필의 필드 누락은 false |
객체 목록은 putParcelableArrayList, 관심사 문자열 목록은 putStringArrayList로 전송한다. JSON 문자열을 대신 넣지 않는다. 프로필 목록 검증 상한은 profiles 64개다. 실패 시 성공 필드 일부가 없을 수 있다.
삭제 성공 후 DeviceAgent는 같은 voiceId의 대기 사본 제거를 시도한다. 제거 실패가 원래 성공 응답에 반영되지 않을 수 있으며 전체 삭제 트랜잭션을 보장하지 않는다.
8. 구현 시 반드시 구분할 것
- 등록 제어는 DeviceAgent가 중계하지만 녹음은 AI-Agent가 소유한다.
- 상태 이벤트, commit ACK, 관리 결과는 서로 다른 method와 성공 판정 기준을 사용한다.
- DeviceAgent의 UID 패키지 검사는 공유 system UID 내부 앱을 완전히 분리하지 못한다.
- 중복 start, 생성 중 취소, 지연 작업, 상태 순번, 재시도·재부팅 복구는 현재 코드의 보완 대상이다.
- Bundle 상수와 ModuleCmdCallbackPolicy 허용 목록을 함께 적용해야 callback이 도착한다.
9. 요청별 수명과 완료 판정
이 절은 2026-09-18 작업본의 함수 흐름을 확인한 결과다. Binder 호출 반환, DeviceAgent 보관, 서버 저장 완료는 서로 다른 결과다.
| 작업 | 요청을 보내는 쪽 | 상관관계·보관 주체 | 완료로 판단하는 결과 | 현재 한계 |
|---|---|---|---|---|
| 등록 제어 | 등록 App → DeviceAgent → AI-Agent | AI-Agent의 등록 세션 requestId |
AI-Agent가 발행한 voiceProfileStateChanged |
DeviceAgent의 0만으로 녹음 시작·종료를 확정하지 않음 |
| 프로필 전달 | AI-Agent → DeviceAgent | AI-Agent pendingVoiceProfileDelivery; DeviceAgent 전달 대기 저장소 |
같은 요청의 voiceProfileCommitResult에서 accepted && cloudSynced |
실제 서버 저장 ACK 연계는 미완료 |
| 프로필 조회·삭제 | IoTAgent → DeviceAgent → AI-Agent | 최초 요청의 requestId를 결과까지 유지 |
해당 관리 결과의 success, reasonCode |
요청 수락과 파일·DB·모델 반영 완료를 분리해야 함 |
| 관심사 조회 | AI-Agent → DeviceAgent → IoTAgent → 서버 | AI-Agent의 단일 대기 요청과 임시 스냅샷 | 유효 시간 내 같은 requestId, voiceId의 voicePersonalizationResult |
요청 생성부터 5초; 만료·취소·중복 응답은 적용하지 않음 |
프로필 전달 결과에 따른 처리
| 받은 값·상황 | AI-Agent의 현재 처리 | 화면·연동에서 해석할 의미 |
|---|---|---|
accepted=true, cloudSynced=true |
pending 제거, COMPLETED, terminal=true, 오디오 복구 |
등록 완료. 단, 로컬 시험 ACK 설정인지 함께 구분 |
accepted=true, cloudSynced=false |
pending 유지, DELIVERY_FAILED, 전달받은 사유 또는 WAITING_CLOUD_SYNC |
기기 전달 대기 저장소에는 들어갔지만 서버 완료는 아님 |
accepted=false |
pending 유지, DELIVERY_FAILED |
실패 사유를 확인하고 전달 재시도 가능 여부 판단 |
| 다른 요청의 ACK / 세션·pending 없음 | ACK 무시 | 이전 요청 결과로 현재 화면을 완료시키지 않음 |
| ACK가 오지 않음 | 현재 전용 ACK 타임아웃 없음 | 계속 대기할 수 있음. 5초 관심사 조회 제한과 혼동하지 않음 |
예를 들어 requestId=registration-demo-01의 저장 결과가 accepted=true, cloudSynced=false이면 모바일 App에 “서버 등록 완료”를 보여주면 안 된다. persist.sys.vp.local_ack=true는 로컬 시험을 위해 완료를 흉내 내는 설정이지 실제 AWS 저장 증거가 아니다.
책임과 재시도 범위
- AI-Agent는 상태와 프로필을 생성한다. DeviceAgent는 호출자·입력 검증 후 Bundle 복사본을 대상 callback으로 전달한다. 상태를 자체적으로 다음 단계로 바꾸지 않는다.
retryVoiceProfileDelivery는 기존 pending 프로필을 다시 보낸다. 전체startVoiceProfileEnrollment재호출은 새 녹음 흐름을 시작할 수 있으므로 전달 재시도 대용으로 쓰지 않는다.- 현재 AI-Agent의 전송
runCatching(...).isSuccess는 호출 중 예외가 없었다는 뜻이다. 업무 완료 ACK를 받은 뜻이 아니다. - 생성 중 취소와 늦은 생성 완료의 충돌은 내부 구조의 비동기 수명에 별도로 기재했다. ACK의 요청 ID 검사만으로 이 경합까지 해결되지는 않는다.
인터페이스 시험표
아래는 구현 검토에서 도출한 시험 조건이며 실기기 통과 결과가 아니다.
| 번호 | 입력·상황 | 확인할 결과 |
|---|---|---|
| AD-01 | 정상 commit 후 accepted=true, cloudSynced=false |
COMPLETED 금지, pending 유지, DELIVERY_FAILED 사유 전달 |
| AD-02 | 다른 requestId의 commit ACK | 현재 세션·화면·pending 불변 |
| AD-03 | 같은 관심사 응답을 두 번 전달 | 최초 유효 결과만 적용 |
| AD-04 | 관심사 응답을 요청 생성 5초 이후 전달 | 만료 응답 폐기; 과거 로컬 관심사로 대체하지 않음 |
| AD-05 | DeviceAgent 수락 직후 대상 서비스 연결 종료 | Binder 수락과 실제 callback 도착을 각각 기록; 완료로 오인하지 않음 |
| AD-06 | 프로필 전달 실패 후 전달만 재시도 | 동일 voiceId 유지, 녹음·UUID 생성 반복 없음 |
구현 근거·연결 문서
2026-09-18 작업본의 DeviceAgent.java sendModuleCommand와 VoiceProfileContract.java, AI-Agent VoiceProfileIpcContract.kt를 대조했다. 문서·정적 검토이며 새 실기기 시험 결과는 아니다.