@1mancompany/onemancompany 0.7.77 → 0.7.80
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/frontend/app.js +135 -17
- package/frontend/index.html +5 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/onemancompany/api/routes.py +18 -11
- package/src/onemancompany/core/attachments.py +53 -0
- package/src/onemancompany/core/conversation_adapters.py +3 -0
- package/src/onemancompany/onboard.py +5 -3
package/frontend/app.js
CHANGED
|
@@ -2265,12 +2265,14 @@ class AppController {
|
|
|
2265
2265
|
this._inputHistoryIdx = this._inputHistory.length;
|
|
2266
2266
|
const doSend = async () => {
|
|
2267
2267
|
const text = (input?.value || '').trim();
|
|
2268
|
-
|
|
2268
|
+
const hasPendingFiles = (this._ceoPendingFiles || []).length > 0;
|
|
2269
|
+
if (!text && !hasPendingFiles) return;
|
|
2269
2270
|
|
|
2270
2271
|
// Show typing indicator while waiting for agent response
|
|
2271
2272
|
this._showCeoTyping();
|
|
2272
2273
|
|
|
2273
|
-
// Execute slash command if input starts with /command
|
|
2274
|
+
// Execute slash command if input starts with /command (slash actions
|
|
2275
|
+
// pick up pending files via _attachPendingFilesToFormData when needed).
|
|
2274
2276
|
if (text.startsWith('/')) {
|
|
2275
2277
|
const cmdText = text.split(' ')[0].toLowerCase();
|
|
2276
2278
|
const argText = text.slice(cmdText.length).trim();
|
|
@@ -2284,8 +2286,8 @@ class AppController {
|
|
|
2284
2286
|
}
|
|
2285
2287
|
}
|
|
2286
2288
|
|
|
2287
|
-
// Save to input history
|
|
2288
|
-
if (!this._inputHistory.length || this._inputHistory[this._inputHistory.length - 1] !== text) {
|
|
2289
|
+
// Save to input history (only when there is actual text)
|
|
2290
|
+
if (text && (!this._inputHistory.length || this._inputHistory[this._inputHistory.length - 1] !== text)) {
|
|
2289
2291
|
this._inputHistory.push(text);
|
|
2290
2292
|
if (this._inputHistory.length > 100) this._inputHistory.shift();
|
|
2291
2293
|
localStorage.setItem('ceo-input-history', JSON.stringify(this._inputHistory));
|
|
@@ -2293,8 +2295,20 @@ class AppController {
|
|
|
2293
2295
|
this._inputHistoryIdx = this._inputHistory.length;
|
|
2294
2296
|
input.value = '';
|
|
2295
2297
|
|
|
2298
|
+
// Snapshot pending files for this send (display chip + dispatch)
|
|
2299
|
+
const pendingFiles = this._ceoPendingFiles || [];
|
|
2300
|
+
const fileNames = pendingFiles.map(f => f.name);
|
|
2301
|
+
|
|
2296
2302
|
// Show CEO message immediately in terminal
|
|
2297
|
-
|
|
2303
|
+
const displayText = text || '(attachment)';
|
|
2304
|
+
this._ceoTerm?.appendCeoMessage(displayText);
|
|
2305
|
+
if (fileNames.length) {
|
|
2306
|
+
this._ceoTerm?.appendMessage({
|
|
2307
|
+
role: 'system',
|
|
2308
|
+
text: `\u{1F4CE} ${fileNames.join(', ')}`,
|
|
2309
|
+
source: 'upload',
|
|
2310
|
+
});
|
|
2311
|
+
}
|
|
2298
2312
|
|
|
2299
2313
|
// /iter mode: create new iteration on pending project
|
|
2300
2314
|
if (this._pendingIterProject) {
|
|
@@ -2308,6 +2322,7 @@ class AppController {
|
|
|
2308
2322
|
formData.append('mode', 'standard');
|
|
2309
2323
|
const productId = document.getElementById('ceo-product-select')?.value || '';
|
|
2310
2324
|
if (productId) formData.append('product_id', productId);
|
|
2325
|
+
this._attachPendingFilesToFormData(formData);
|
|
2311
2326
|
await fetch('/api/ceo/task', { method: 'POST', body: formData });
|
|
2312
2327
|
await this._refreshCeoProjectList();
|
|
2313
2328
|
this._ceoTerm?.appendMessage({ role: 'system', text: 'New iteration created.', source: 'system' });
|
|
@@ -2316,7 +2331,7 @@ class AppController {
|
|
|
2316
2331
|
return;
|
|
2317
2332
|
}
|
|
2318
2333
|
|
|
2319
|
-
// Meeting mode: send via meeting/chat API
|
|
2334
|
+
// Meeting mode: send via meeting/chat API (no attachment support yet)
|
|
2320
2335
|
if (this._currentConvType === 'meeting') {
|
|
2321
2336
|
try {
|
|
2322
2337
|
const res = await fetch('/api/meeting/chat', {
|
|
@@ -2346,10 +2361,14 @@ class AppController {
|
|
|
2346
2361
|
// 1-on-1 conversation mode: send via conversation API
|
|
2347
2362
|
if (this._currentConvType === 'oneonone' && this._currentConvId) {
|
|
2348
2363
|
try {
|
|
2364
|
+
const uploaded = await this._uploadCeoPendingFiles();
|
|
2349
2365
|
await fetch(`/api/conversation/${this._currentConvId}/message`, {
|
|
2350
2366
|
method: 'POST',
|
|
2351
2367
|
headers: {'Content-Type': 'application/json'},
|
|
2352
|
-
body: JSON.stringify({
|
|
2368
|
+
body: JSON.stringify({
|
|
2369
|
+
text: text || '(attachment)',
|
|
2370
|
+
attachments: uploaded.map(a => a.path),
|
|
2371
|
+
}),
|
|
2353
2372
|
});
|
|
2354
2373
|
} catch (e) { console.error('Failed to send 1-on-1 message:', e); }
|
|
2355
2374
|
input?.focus();
|
|
@@ -2359,10 +2378,14 @@ class AppController {
|
|
|
2359
2378
|
if (this._currentCeoProject === this._EA_CHAT && this._eaChatConvId) {
|
|
2360
2379
|
// EA Chat: send as conversation message — EA decides whether to create project
|
|
2361
2380
|
try {
|
|
2381
|
+
const uploaded = await this._uploadCeoPendingFiles();
|
|
2362
2382
|
await fetch(`/api/conversation/${this._eaChatConvId}/message`, {
|
|
2363
2383
|
method: 'POST',
|
|
2364
2384
|
headers: {'Content-Type': 'application/json'},
|
|
2365
|
-
body: JSON.stringify({
|
|
2385
|
+
body: JSON.stringify({
|
|
2386
|
+
text: text || '(attachment)',
|
|
2387
|
+
attachments: uploaded.map(a => a.path),
|
|
2388
|
+
}),
|
|
2366
2389
|
});
|
|
2367
2390
|
// EA response arrives via WebSocket conversation_message event
|
|
2368
2391
|
} catch (e) { console.error('Failed to send EA chat message:', e); }
|
|
@@ -2376,15 +2399,21 @@ class AppController {
|
|
|
2376
2399
|
formData.append('mode', mode);
|
|
2377
2400
|
const productId2 = document.getElementById('ceo-product-select')?.value || '';
|
|
2378
2401
|
if (productId2) formData.append('product_id', productId2);
|
|
2402
|
+
this._attachPendingFilesToFormData(formData);
|
|
2379
2403
|
await fetch('/api/ceo/task', { method: 'POST', body: formData });
|
|
2380
2404
|
await this._refreshCeoProjectList();
|
|
2381
2405
|
} catch (e) { console.error('Failed to submit task:', e); }
|
|
2382
2406
|
} else {
|
|
2383
2407
|
try {
|
|
2408
|
+
const projectId = this._currentCeoProject.split('/')[0];
|
|
2409
|
+
const uploaded = await this._uploadCeoPendingFiles(projectId);
|
|
2384
2410
|
await fetch(`/api/ceo/sessions/${encodeURIComponent(this._currentCeoProject)}/message`, {
|
|
2385
2411
|
method: 'POST',
|
|
2386
2412
|
headers: {'Content-Type': 'application/json'},
|
|
2387
|
-
body: JSON.stringify({
|
|
2413
|
+
body: JSON.stringify({
|
|
2414
|
+
text: text || '(attachment)',
|
|
2415
|
+
attachments: uploaded.map(a => a.path),
|
|
2416
|
+
}),
|
|
2388
2417
|
});
|
|
2389
2418
|
await this._refreshCeoProjectList();
|
|
2390
2419
|
} catch (e) { console.error('Failed to send:', e); }
|
|
@@ -2471,16 +2500,12 @@ class AppController {
|
|
|
2471
2500
|
this._handleMentionInput(input);
|
|
2472
2501
|
});
|
|
2473
2502
|
|
|
2474
|
-
// File upload
|
|
2503
|
+
// File upload — files queue locally and dispatch with the next message
|
|
2504
|
+
this._ceoPendingFiles = this._ceoPendingFiles || [];
|
|
2475
2505
|
const fileInput = document.getElementById('ceo-file-input');
|
|
2476
2506
|
fileInput?.addEventListener('change', () => {
|
|
2477
2507
|
if (!fileInput.files?.length) return;
|
|
2478
|
-
|
|
2479
|
-
this._ceoTerm?.appendMessage({
|
|
2480
|
-
role: 'system', text: `Attached: ${names}`, source: 'upload',
|
|
2481
|
-
});
|
|
2482
|
-
// Store files for next send
|
|
2483
|
-
this._pendingFiles = Array.from(fileInput.files);
|
|
2508
|
+
this._handleCeoFileSelect(fileInput.files);
|
|
2484
2509
|
fileInput.value = '';
|
|
2485
2510
|
});
|
|
2486
2511
|
|
|
@@ -2574,6 +2599,7 @@ class AppController {
|
|
|
2574
2599
|
const formData = new FormData();
|
|
2575
2600
|
formData.append('task', arg);
|
|
2576
2601
|
formData.append('mode', 'standard');
|
|
2602
|
+
this._attachPendingFilesToFormData(formData);
|
|
2577
2603
|
fetch('/api/ceo/task', { method: 'POST', body: formData })
|
|
2578
2604
|
.then(() => { this._refreshCeoProjectList(); this._ceoTerm?.appendMessage({ role: 'system', text: '✓ Project created', source: 'system' }); })
|
|
2579
2605
|
.catch(e => { this._ceoTerm?.appendMessage({ role: 'system', text: `✗ Failed: ${e.message}`, source: 'system' }); });
|
|
@@ -2595,6 +2621,7 @@ class AppController {
|
|
|
2595
2621
|
formData.append('task', arg);
|
|
2596
2622
|
formData.append('project_id', pid.split('/')[0]);
|
|
2597
2623
|
formData.append('mode', 'standard');
|
|
2624
|
+
this._attachPendingFilesToFormData(formData);
|
|
2598
2625
|
fetch('/api/ceo/task', { method: 'POST', body: formData })
|
|
2599
2626
|
.then(() => { this._refreshCeoProjectList(); this._ceoTerm?.appendMessage({ role: 'system', text: '✓ New iteration created', source: 'system' }); })
|
|
2600
2627
|
.catch(e => { this._ceoTerm?.appendMessage({ role: 'system', text: `✗ Failed: ${e.message}`, source: 'system' }); });
|
|
@@ -2622,6 +2649,7 @@ class AppController {
|
|
|
2622
2649
|
const formData = new FormData();
|
|
2623
2650
|
formData.append('task', arg);
|
|
2624
2651
|
formData.append('mode', 'simple');
|
|
2652
|
+
this._attachPendingFilesToFormData(formData);
|
|
2625
2653
|
fetch('/api/ceo/task', { method: 'POST', body: formData })
|
|
2626
2654
|
.then(() => { this._refreshCeoProjectList(); this._ceoTerm?.appendMessage({ role: 'system', text: '✓ Simple task created', source: 'system' }); })
|
|
2627
2655
|
.catch(e => { this._ceoTerm?.appendMessage({ role: 'system', text: `✗ Failed: ${e.message}`, source: 'system' }); });
|
|
@@ -2668,7 +2696,6 @@ class AppController {
|
|
|
2668
2696
|
{ cmd: '/discuss', desc: 'Start discussion meeting (open floor)', action: async (arg) => {
|
|
2669
2697
|
await this._startMeetingInConsole('discussion', arg);
|
|
2670
2698
|
}},
|
|
2671
|
-
{ cmd: '/attach', desc: 'Attach file or image', action: () => document.getElementById('ceo-file-input')?.click() },
|
|
2672
2699
|
{ cmd: '/clear', desc: 'Clear EA chat history', action: async () => {
|
|
2673
2700
|
if (this._currentCeoProject !== this._EA_CHAT) {
|
|
2674
2701
|
this._ceoTerm?.appendMessage({ role: 'system', text: '/clear only works in EA chat.', source: 'system' });
|
|
@@ -6519,6 +6546,97 @@ class AppController {
|
|
|
6519
6546
|
return uploaded;
|
|
6520
6547
|
}
|
|
6521
6548
|
|
|
6549
|
+
// ===== CEO Console File Upload (queued, dispatched with the next message) =====
|
|
6550
|
+
_handleCeoFileSelect(files) {
|
|
6551
|
+
if (!this._ceoPendingFiles) this._ceoPendingFiles = [];
|
|
6552
|
+
for (const file of files) {
|
|
6553
|
+
const reader = new FileReader();
|
|
6554
|
+
reader.onload = (e) => {
|
|
6555
|
+
let type = 'file';
|
|
6556
|
+
if (file.type.startsWith('image/')) type = 'image';
|
|
6557
|
+
else if (file.type.startsWith('video/')) type = 'video';
|
|
6558
|
+
this._ceoPendingFiles.push({
|
|
6559
|
+
name: file.name,
|
|
6560
|
+
type,
|
|
6561
|
+
dataUrl: e.target.result,
|
|
6562
|
+
file,
|
|
6563
|
+
});
|
|
6564
|
+
this._updateCeoPreviewBar();
|
|
6565
|
+
};
|
|
6566
|
+
reader.readAsDataURL(file);
|
|
6567
|
+
}
|
|
6568
|
+
}
|
|
6569
|
+
|
|
6570
|
+
_updateCeoPreviewBar() {
|
|
6571
|
+
const bar = document.getElementById('ceo-preview-bar');
|
|
6572
|
+
if (!bar) return;
|
|
6573
|
+
if (!this._ceoPendingFiles || !this._ceoPendingFiles.length) {
|
|
6574
|
+
bar.classList.add('hidden');
|
|
6575
|
+
bar.innerHTML = '';
|
|
6576
|
+
return;
|
|
6577
|
+
}
|
|
6578
|
+
bar.classList.remove('hidden');
|
|
6579
|
+
bar.innerHTML = '';
|
|
6580
|
+
this._ceoPendingFiles.forEach((f, idx) => {
|
|
6581
|
+
const item = document.createElement('div');
|
|
6582
|
+
item.className = 'chat-preview-item';
|
|
6583
|
+
if (f.type === 'image') {
|
|
6584
|
+
item.innerHTML = `<img class="chat-preview-thumb" src="${f.dataUrl}" alt="${this._escapeHtml(f.name)}" />`;
|
|
6585
|
+
} else if (f.type === 'video') {
|
|
6586
|
+
item.innerHTML = `<div class="chat-preview-file">\u{1F3AC}<br>${this._escapeHtml(f.name.substring(0, 12))}</div>`;
|
|
6587
|
+
} else {
|
|
6588
|
+
item.innerHTML = `<div class="chat-preview-file">\u{1F4C4}<br>${this._escapeHtml(f.name.substring(0, 12))}</div>`;
|
|
6589
|
+
}
|
|
6590
|
+
const removeBtn = document.createElement('button');
|
|
6591
|
+
removeBtn.className = 'chat-preview-remove';
|
|
6592
|
+
removeBtn.textContent = '×';
|
|
6593
|
+
removeBtn.title = `Remove ${f.name}`;
|
|
6594
|
+
removeBtn.onclick = () => {
|
|
6595
|
+
this._ceoPendingFiles.splice(idx, 1);
|
|
6596
|
+
this._updateCeoPreviewBar();
|
|
6597
|
+
};
|
|
6598
|
+
item.appendChild(removeBtn);
|
|
6599
|
+
bar.appendChild(item);
|
|
6600
|
+
});
|
|
6601
|
+
}
|
|
6602
|
+
|
|
6603
|
+
async _uploadCeoPendingFiles(projectId = '') {
|
|
6604
|
+
if (!this._ceoPendingFiles || !this._ceoPendingFiles.length) return [];
|
|
6605
|
+
const uploaded = [];
|
|
6606
|
+
const url = projectId
|
|
6607
|
+
? `/api/upload?project_id=${encodeURIComponent(projectId)}`
|
|
6608
|
+
: '/api/upload';
|
|
6609
|
+
for (const f of this._ceoPendingFiles) {
|
|
6610
|
+
const formData = new FormData();
|
|
6611
|
+
formData.append('file', f.file, f.name);
|
|
6612
|
+
try {
|
|
6613
|
+
const resp = await fetch(url, { method: 'POST', body: formData });
|
|
6614
|
+
const data = await resp.json();
|
|
6615
|
+
if (data.path) {
|
|
6616
|
+
uploaded.push({
|
|
6617
|
+
path: data.path,
|
|
6618
|
+
filename: data.filename || f.name,
|
|
6619
|
+
content_type: data.content_type || '',
|
|
6620
|
+
});
|
|
6621
|
+
}
|
|
6622
|
+
} catch (err) {
|
|
6623
|
+
console.error('CEO upload failed:', err);
|
|
6624
|
+
}
|
|
6625
|
+
}
|
|
6626
|
+
this._ceoPendingFiles = [];
|
|
6627
|
+
this._updateCeoPreviewBar();
|
|
6628
|
+
return uploaded;
|
|
6629
|
+
}
|
|
6630
|
+
|
|
6631
|
+
_attachPendingFilesToFormData(formData) {
|
|
6632
|
+
if (!this._ceoPendingFiles || !this._ceoPendingFiles.length) return;
|
|
6633
|
+
for (const f of this._ceoPendingFiles) {
|
|
6634
|
+
formData.append('files', f.file, f.name);
|
|
6635
|
+
}
|
|
6636
|
+
this._ceoPendingFiles = [];
|
|
6637
|
+
this._updateCeoPreviewBar();
|
|
6638
|
+
}
|
|
6639
|
+
|
|
6522
6640
|
// ===== Tool Detail — Dynamic Section Renderer Framework =====
|
|
6523
6641
|
//
|
|
6524
6642
|
// Each tool's definition returns a `sections` array from the backend.
|
package/frontend/index.html
CHANGED
|
@@ -117,9 +117,13 @@
|
|
|
117
117
|
<option value="">No Product</option>
|
|
118
118
|
</select>
|
|
119
119
|
</div>
|
|
120
|
+
<div id="ceo-preview-bar" class="chat-preview-bar hidden"></div>
|
|
120
121
|
<div id="ceo-conv-input-row">
|
|
122
|
+
<label class="chat-attach-btn" id="ceo-attach-btn" title="Attach file or image — sent with your next message">
|
|
123
|
+
<input type="file" id="ceo-file-input" multiple hidden />
|
|
124
|
+
📎
|
|
125
|
+
</label>
|
|
121
126
|
<textarea id="ceo-conv-input" placeholder="$ Type message, / for commands (Enter to send)" rows="1" aria-label="CEO message input"></textarea>
|
|
122
|
-
<input type="file" id="ceo-file-input" multiple hidden />
|
|
123
127
|
</div>
|
|
124
128
|
<div id="ceo-slash-menu" class="hidden"></div>
|
|
125
129
|
</div>
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
import json as _json
|
|
6
7
|
import re
|
|
7
8
|
import shutil
|
|
8
9
|
import uuid as _uuid
|
|
@@ -136,6 +137,11 @@ def _save_file_deduped(upload_dir: Path, filename: str, content: bytes) -> Path:
|
|
|
136
137
|
return dest
|
|
137
138
|
|
|
138
139
|
|
|
140
|
+
from onemancompany.core.attachments import (
|
|
141
|
+
build_attachment_prompt as _build_attachment_prompt,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
139
145
|
def _get_employee_manager():
|
|
140
146
|
"""Lazy import to avoid circular dependency."""
|
|
141
147
|
from onemancompany.core.vessel import employee_manager
|
|
@@ -572,10 +578,7 @@ async def ceo_submit_task(
|
|
|
572
578
|
ctx_id = f"{pid}/{iter_id}" if iter_id else pid
|
|
573
579
|
|
|
574
580
|
# Build attachment info string for EA
|
|
575
|
-
attach_info =
|
|
576
|
-
if attachments:
|
|
577
|
-
lines = [f"- Attachment: {a['filename']} (saved at {a['path']})" for a in attachments]
|
|
578
|
-
attach_info = "\n\nCEO attached the following files:\n" + "\n".join(lines)
|
|
581
|
+
attach_info = _build_attachment_prompt(attachments)
|
|
579
582
|
|
|
580
583
|
loop = get_agent_loop(EA_ID)
|
|
581
584
|
if loop:
|
|
@@ -723,6 +726,9 @@ async def task_followup(project_id: str, body: dict) -> dict:
|
|
|
723
726
|
if work_summary_lines:
|
|
724
727
|
context_parts.append(f"Previous work results:\n" + "\n".join(work_summary_lines) + "\n")
|
|
725
728
|
context_parts.append(f"CEO follow-up instructions: {instructions}\n")
|
|
729
|
+
attach_info = _build_attachment_prompt(body.get("attachments") or [])
|
|
730
|
+
if attach_info:
|
|
731
|
+
context_parts.append(attach_info.lstrip("\n") + "\n")
|
|
726
732
|
context_parts.append(
|
|
727
733
|
f"\nBuild on the existing work — do NOT redo completed subtasks unless the CEO explicitly asks."
|
|
728
734
|
f" Use dispatch_child() if subtasks are needed.\n\n"
|
|
@@ -833,10 +839,7 @@ async def oneonone_chat(body: dict) -> dict:
|
|
|
833
839
|
return {"error": f"Employee '{employee_id}' not found"}
|
|
834
840
|
|
|
835
841
|
# Build attachment info string for prompt injection
|
|
836
|
-
attach_info =
|
|
837
|
-
if attachments:
|
|
838
|
-
lines = [f"- Attachment: {a.get('filename', 'file')} (saved at {a.get('path', '')})" for a in attachments]
|
|
839
|
-
attach_info = "\n\nCEO attached the following files:\n" + "\n".join(lines)
|
|
842
|
+
attach_info = _build_attachment_prompt(attachments)
|
|
840
843
|
|
|
841
844
|
# On first message (empty history), mark employee as in meeting
|
|
842
845
|
if not history and emp_data:
|
|
@@ -908,7 +911,7 @@ async def oneonone_chat(body: dict) -> dict:
|
|
|
908
911
|
messages.append(HumanMessage(content=entry["content"]))
|
|
909
912
|
elif entry.get("role") == "employee":
|
|
910
913
|
messages.append(AIMessage(content=entry["content"]))
|
|
911
|
-
messages.append(HumanMessage(content=message))
|
|
914
|
+
messages.append(HumanMessage(content=message + attach_info))
|
|
912
915
|
|
|
913
916
|
llm = make_llm(employee_id)
|
|
914
917
|
result = await _llm_invoke_with_retry(llm, messages, category="oneonone", employee_id=employee_id)
|
|
@@ -6364,13 +6367,17 @@ async def send_ceo_session_message(project_id: str, body: dict):
|
|
|
6364
6367
|
|
|
6365
6368
|
# Persist CEO message (masked for credential requests)
|
|
6366
6369
|
display_text = result.get("display_text", text)
|
|
6367
|
-
await service.send_message(
|
|
6370
|
+
await service.send_message(
|
|
6371
|
+
conv.id, "ceo", "CEO", display_text,
|
|
6372
|
+
mentions=mentions, attachments=body.get("attachments") or [],
|
|
6373
|
+
)
|
|
6368
6374
|
|
|
6369
6375
|
if result["type"] == "followup":
|
|
6370
6376
|
# Dispatch as a CEO_FOLLOWUP via the existing task_followup logic.
|
|
6371
6377
|
try:
|
|
6372
6378
|
followup_result = await task_followup(
|
|
6373
|
-
project_id,
|
|
6379
|
+
project_id,
|
|
6380
|
+
{"instructions": text, "attachments": body.get("attachments") or []},
|
|
6374
6381
|
)
|
|
6375
6382
|
result["followup"] = followup_result
|
|
6376
6383
|
result["message"] = "Follow-up instruction dispatched"
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Shared helpers for rendering attachment instructions into LLM prompts.
|
|
2
|
+
|
|
3
|
+
The CEO submits attachments through multiple paths (task creation, 1-on-1
|
|
4
|
+
chat, EA / project conversations). All of them need to tell the agent to
|
|
5
|
+
read each attached file before responding. The renderer is centralized
|
|
6
|
+
here so prompt wording and quoting rules stay consistent.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json as _json
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def build_attachment_prompt(attachments: list[dict]) -> str:
|
|
16
|
+
"""Describe attachments with both display and read-safe path formats.
|
|
17
|
+
|
|
18
|
+
The plain path in ``(saved at ...)`` is for human readability. The
|
|
19
|
+
``read("...")`` argument and attachment display name use JSON quoting so
|
|
20
|
+
backslashes, quotes, and newlines stay safe/parsable without inventing
|
|
21
|
+
custom escaping rules.
|
|
22
|
+
"""
|
|
23
|
+
if not attachments:
|
|
24
|
+
return ""
|
|
25
|
+
|
|
26
|
+
lines: list[str] = []
|
|
27
|
+
for attachment in attachments:
|
|
28
|
+
raw_filename = attachment.get("filename", "file")
|
|
29
|
+
quoted_filename = _json.dumps("file" if raw_filename is None else str(raw_filename))
|
|
30
|
+
raw_path = attachment.get("path", "")
|
|
31
|
+
path = "" if raw_path is None else str(raw_path).strip()
|
|
32
|
+
if path:
|
|
33
|
+
lines.append(f"- Attachment: {quoted_filename} (saved at {path}) [read({_json.dumps(path)})]")
|
|
34
|
+
else:
|
|
35
|
+
lines.append(f"- Attachment: {quoted_filename}")
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
"\n\nCEO attached the following files:\n"
|
|
39
|
+
+ "\n".join(lines)
|
|
40
|
+
+ "\nRead each attachment with read() before responding so you can inspect the actual file contents."
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def build_attachment_prompt_from_paths(paths: list[str]) -> str:
|
|
45
|
+
"""Same as :func:`build_attachment_prompt`, but accepts bare path strings.
|
|
46
|
+
|
|
47
|
+
Used by the conversation flow where ``Message.attachments`` stores just
|
|
48
|
+
saved-file paths. The display filename is derived from the basename.
|
|
49
|
+
"""
|
|
50
|
+
if not paths:
|
|
51
|
+
return ""
|
|
52
|
+
attachments = [{"filename": Path(p).name, "path": p} for p in paths if p]
|
|
53
|
+
return build_attachment_prompt(attachments)
|
|
@@ -151,6 +151,9 @@ def _build_conversation_prompt(
|
|
|
151
151
|
lines.append(f"[{msg.role}]: {msg.text}")
|
|
152
152
|
|
|
153
153
|
lines.append(f"\n[{new_message.role}]: {new_message.text}")
|
|
154
|
+
if new_message.attachments:
|
|
155
|
+
from onemancompany.core.attachments import build_attachment_prompt_from_paths
|
|
156
|
+
lines.append(build_attachment_prompt_from_paths(list(new_message.attachments)))
|
|
154
157
|
lines.append("\nPlease respond:")
|
|
155
158
|
return "\n".join(lines)
|
|
156
159
|
|
|
@@ -741,7 +741,7 @@ def _step_execute(
|
|
|
741
741
|
|
|
742
742
|
# 6. Generate MCP configs for founding employees
|
|
743
743
|
with console.status(" Generating MCP configs..."):
|
|
744
|
-
_generate_mcp_configs(extras.get(ENV_KEY_SKILLSMP, ""))
|
|
744
|
+
_generate_mcp_configs(extras.get(ENV_KEY_SKILLSMP, ""), host, port)
|
|
745
745
|
console.print(" [green]\u2714[/green] MCP configs generated for founding employees")
|
|
746
746
|
|
|
747
747
|
# 7. Apply agent family (hosting) assignments to founding employees
|
|
@@ -830,13 +830,15 @@ def _assign_default_avatars(console: Console) -> None:
|
|
|
830
830
|
console.print(" [green]\u2714[/green] Founding employees already have avatars")
|
|
831
831
|
|
|
832
832
|
|
|
833
|
-
def _generate_mcp_configs(skillsmp_key: str) -> None:
|
|
833
|
+
def _generate_mcp_configs(skillsmp_key: str, host: str, port: int) -> None:
|
|
834
834
|
"""Generate mcp_config.json for founding employees."""
|
|
835
835
|
import sys
|
|
836
836
|
|
|
837
837
|
python_path = sys.executable
|
|
838
838
|
from onemancompany.core.config import EXEC_IDS
|
|
839
839
|
exec_ids = sorted(EXEC_IDS)
|
|
840
|
+
server_host = "localhost" if host == "0.0.0.0" else host
|
|
841
|
+
server_url = f"http://{server_host}:{port}"
|
|
840
842
|
|
|
841
843
|
for emp_id in exec_ids:
|
|
842
844
|
emp_dir = EMPLOYEES_DIR / emp_id
|
|
@@ -852,7 +854,7 @@ def _generate_mcp_configs(skillsmp_key: str) -> None:
|
|
|
852
854
|
ENV_OMC_TASK_ID: "",
|
|
853
855
|
ENV_OMC_PROJECT_ID: "",
|
|
854
856
|
ENV_OMC_PROJECT_DIR: "",
|
|
855
|
-
ENV_OMC_SERVER_URL:
|
|
857
|
+
ENV_OMC_SERVER_URL: server_url,
|
|
856
858
|
},
|
|
857
859
|
},
|
|
858
860
|
}
|