fuli-context 0.7.9 → 0.7.10-beta.7
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/README.md +5 -2
- package/README.zh-CN.md +5 -3
- package/dist/web/assets/{AboutPage-Bp-Xz6yo.js → AboutPage-EtWPOSpz.js} +1 -1
- package/dist/web/assets/AboutPage-vYIBifKw.css +1 -0
- package/dist/web/assets/AgentHand-D5pOgVjJ.js +1 -0
- package/dist/web/assets/AgentHand-DvdNtJ3_.css +1 -0
- package/dist/web/assets/ConnectionsPage-DXkvMuyK.js +1 -0
- package/dist/web/assets/EmployeeRecruitDialog-B59X3rOi.css +1 -0
- package/dist/web/assets/EmployeeRecruitDialog-DONxXnd2.js +1 -0
- package/dist/web/assets/EmployeeWorkbenchPage-Cd4hd4bf.js +3 -0
- package/dist/web/assets/EmployeeWorkbenchPage-Cz7y-IFD.css +1 -0
- package/dist/web/assets/GrowthLoading-B_Hh0CRS.css +1 -0
- package/dist/web/assets/GrowthLoading-CCYPgE2f.js +1 -0
- package/dist/web/assets/KnowledgeEditDialog-BUqMfa0a.js +1 -0
- package/dist/web/assets/KnowledgeEditDialog-D9NuRHvp.css +1 -0
- package/dist/web/assets/KnowledgeInspector-DlVPDGP3.js +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-DhthZGKZ.js +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-h2iKvQpO.css +1 -0
- package/dist/web/assets/KnowledgePage-B6RDVxq-.js +1 -0
- package/dist/web/assets/KnowledgeWorkspace-mVFHQcxr.js +4 -0
- package/dist/web/assets/KnowledgeWorkspace-sb8OJ97I.css +1 -0
- package/dist/web/assets/NotFoundPage-CCeEdBy9.js +1 -0
- package/dist/web/assets/{OverviewPage-HwoSW-5z.js → OverviewPage-CnYWoZ3r.js} +1 -1
- package/dist/web/assets/PersonalProfilePage-Bz4zpTPC.js +1 -0
- package/dist/web/assets/PersonalProfilePage-yQRCBude.css +1 -0
- package/dist/web/assets/PersonalProjectsPage-2u1o6dvA.js +1 -0
- package/dist/web/assets/ProjectAgentsPage-8tci631q.js +11 -0
- package/dist/web/assets/ProjectAgentsPage-BN4k3ubu.css +1 -0
- package/dist/web/assets/ProjectScopePicker-BcPtIcv8.js +1 -0
- package/dist/web/assets/ProjectScopePicker-BvaGTMFx.css +1 -0
- package/dist/web/assets/PublicProjectsPage-BmBHfnud.js +1 -0
- package/dist/web/assets/ReviewPage-tvt2eqhw.js +1 -0
- package/dist/web/assets/{SearchableSelect-CFZNdtlY.js → SearchableSelect-BjBDBVuZ.js} +1 -1
- package/dist/web/assets/SettingsPage-Brl-SL33.css +1 -0
- package/dist/web/assets/SettingsPage-D9mNzBg-.js +1 -0
- package/dist/web/assets/SharedJefaReportPage-DXbj_JhO.css +1 -0
- package/dist/web/assets/SharedJefaReportPage-Dn_HBgaa.js +1 -0
- package/dist/web/assets/WritingTastePage-BCwimXC-.js +1 -0
- package/dist/web/assets/WritingTastePage-tb_pgude.css +1 -0
- package/dist/web/assets/bole-avatar-CVIr-bU_.webp +0 -0
- package/dist/web/assets/client-v8XKgQji.js +1 -0
- package/dist/web/assets/console-C9HS-zbd.js +1 -0
- package/dist/web/assets/conversation-launcher-settings-DMDCpf_N.js +1 -0
- package/dist/web/assets/employee-dialog-Bh5ccIbK.css +1 -0
- package/dist/web/assets/{i18n-DQH9D35M.js → i18n-BkJjUsz_.js} +3 -3
- package/dist/web/assets/{identity-JjZG6aS5.js → identity-74yGiMzO.js} +1 -1
- package/dist/web/assets/index-CwjxeHGk.css +1 -0
- package/dist/web/assets/index-Dioqmmvf.js +2 -0
- package/dist/web/assets/{model-zL97NJ-U.js → model-86tZgY0r.js} +1 -1
- package/dist/web/assets/shared-report-CLg82y4D.js +1 -0
- package/dist/web/assets/shared-report-ezeb8dW4.css +1 -0
- package/dist/web/assets/{useApi-CROJJdhE-CElDxEZI.js → useApi-CROJJdhE-BBUO_YEu.js} +1 -1
- package/dist/web/assets/useModalDialog-DrLhqtjR.js +1 -0
- package/dist/web/assets/{vue-router-F7MnkVlW.js → vue-router-Cw4QZxM1.js} +1 -1
- package/dist/web/index.html +14 -8
- package/docs/agent-interface-architecture.md +97 -0
- package/docs/employee-agents.md +58 -2
- package/graph-provider/fuli_graph/collaboration_context.py +2 -1
- package/graph-provider/fuli_graph/graph_models.py +5 -3
- package/graph-provider/fuli_graph/graph_query.py +4 -0
- package/graph-provider/fuli_graph/knowledge_search.py +2 -1
- package/graph-provider/fuli_graph/project_agent_access.py +2 -0
- package/graph-provider/fuli_graph/project_agent_attention_models.py +64 -0
- package/graph-provider/fuli_graph/project_agent_attention_routes.py +38 -0
- package/graph-provider/fuli_graph/project_agent_models.py +2 -0
- package/graph-provider/fuli_graph/project_agent_names.py +33 -0
- package/graph-provider/fuli_graph/project_agent_routes.py +12 -0
- package/graph-provider/fuli_graph/runtime.py +49 -20
- package/graph-provider/fuli_graph/store.py +2 -0
- package/graph-provider/fuli_graph/store_knowledge.py +4 -2
- package/graph-provider/fuli_graph/store_project_agent_attention.py +138 -0
- package/graph-provider/fuli_graph/store_project_agent_executor_learning.py +8 -0
- package/graph-provider/fuli_graph/store_project_agent_memory.py +40 -13
- package/graph-provider/fuli_graph/store_project_agent_task_recruitment.py +4 -1
- package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +4 -1
- package/graph-provider/fuli_graph/store_project_agent_tasks.py +3 -0
- package/graph-provider/fuli_graph/store_project_agents.py +109 -0
- package/graph-provider/fuli_graph/store_task_contexts.py +1 -1
- package/graph-provider/fuli_graph/system_hr_identity.py +218 -0
- package/graph-provider/pyproject.toml +1 -1
- package/npm-shrinkwrap.json +52 -60
- package/package.json +2 -2
- package/skills/operating-fuli/SKILL.md +47 -0
- package/skills/operating-fuli/agents/openai.yaml +4 -0
- package/skills/operating-fuli/references/workflow-recipes.md +33 -0
- package/src/agent-tools/agent-attention-definitions.js +33 -0
- package/src/agent-tools/graph-definitions.js +8 -2
- package/src/agent-tools/graph-handlers.js +41 -0
- package/src/agent-tools/interface-catalog.js +101 -0
- package/src/agent-tools/interface-definitions.js +184 -0
- package/src/agent-tools/project-agent-definitions.js +1 -0
- package/src/cli/local-runtime-command.js +2 -1
- package/src/employees/catalog/bole.json +19 -0
- package/src/employees/install-package.js +3 -0
- package/src/employees/manifest.js +4 -0
- package/src/employees/package-registry.js +2 -0
- package/src/employees/recruitment.js +3 -0
- package/src/employees/service.js +10 -0
- package/src/external-knowledge/registry.js +24 -0
- package/src/external-knowledge/service.js +34 -1
- package/src/graphiti/agent-attention.js +26 -0
- package/src/graphiti/agent-knowledge-workflows.js +1 -0
- package/src/graphiti/project-agent-control-plane.js +21 -0
- package/src/graphiti/project-agent-mapping.js +6 -0
- package/src/graphiti/provider-client.js +13 -0
- package/src/graphiti/workspace-provider-client.js +9 -1
- package/src/http/agent-attention-api-router.js +28 -0
- package/src/http/error-mapping.js +3 -0
- package/src/http/graph-api-router.js +2 -0
- package/src/local-runtime/lifecycle.js +8 -2
- package/src/mcp/create-mcp-server.js +10 -1
- package/src/mcp/tool-annotations.js +31 -0
- package/src/mcp/tool-result.js +4 -4
- package/src/setup/agent-installation-status.js +2 -1
- package/src/setup/agents.js +4 -0
- package/src/setup/graph-runtime.js +4 -0
- package/src/setup/paths.js +2 -1
- package/src/setup/setup.js +4 -2
- package/src/setup/uninstall.js +4 -2
- package/src/system/resource-monitor.js +11 -1
- package/src/system/runtime-settings.js +7 -0
- package/dist/web/assets/AboutPage-DXh7Fwxt.css +0 -1
- package/dist/web/assets/ConnectionsPage-BMpkYeSE.js +0 -1
- package/dist/web/assets/EmployeeRecruitDialog-CnA7rOnB.css +0 -1
- package/dist/web/assets/EmployeeRecruitDialog-DbMVuLAI.js +0 -1
- package/dist/web/assets/EmployeeWorkbenchPage-BpkqpEs7.css +0 -1
- package/dist/web/assets/EmployeeWorkbenchPage-DtXSJOpq.js +0 -1
- package/dist/web/assets/KnowledgeEditDialog-DADXj_BT.js +0 -1
- package/dist/web/assets/KnowledgeEditDialog-iv-dgUeD.css +0 -1
- package/dist/web/assets/KnowledgeInspector-Xs4VCUg8.js +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-B_Ct4bB2.js +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-Cx1w1jb2.css +0 -1
- package/dist/web/assets/KnowledgePage-RG9Ttnrp.js +0 -1
- package/dist/web/assets/KnowledgeWorkspace-DI8YqTVI.js +0 -4
- package/dist/web/assets/PersonalProfilePage-B7T09bow.css +0 -1
- package/dist/web/assets/PersonalProfilePage-DCoG52SW.js +0 -1
- package/dist/web/assets/PersonalProjectsPage-CNb9mlbp.js +0 -1
- package/dist/web/assets/ProjectAgentsPage-C3vW6wmk.css +0 -1
- package/dist/web/assets/ProjectAgentsPage-zQ9BZW9v.js +0 -11
- package/dist/web/assets/PublicProjectsPage-BuLqkZ8g.js +0 -1
- package/dist/web/assets/ReviewPage-8A7N5pJs.js +0 -1
- package/dist/web/assets/SettingsPage-4ah1u906.js +0 -1
- package/dist/web/assets/SettingsPage-CwZp2ka4.css +0 -1
- package/dist/web/assets/WritingTastePage-DSgvTH6x.css +0 -1
- package/dist/web/assets/WritingTastePage-DztRj6MK.js +0 -1
- package/dist/web/assets/avatars-LvVKbDF7.js +0 -1
- package/dist/web/assets/console-B7dWYAoU.js +0 -1
- package/dist/web/assets/conversation-launcher-settings-D1xhW1S5.js +0 -1
- package/dist/web/assets/index-Ch1jmkyO.css +0 -1
- package/dist/web/assets/index-DRd-2Tii.js +0 -2
|
@@ -32,7 +32,7 @@ class StoreKnowledge:
|
|
|
32
32
|
self, actor, space, request.personal_project_id
|
|
33
33
|
)
|
|
34
34
|
if request.project_agent_id:
|
|
35
|
-
await authorize_project_agent(
|
|
35
|
+
agent = await authorize_project_agent(
|
|
36
36
|
self,
|
|
37
37
|
actor,
|
|
38
38
|
space,
|
|
@@ -41,6 +41,7 @@ class StoreKnowledge:
|
|
|
41
41
|
require_active=True,
|
|
42
42
|
require_memory=True,
|
|
43
43
|
)
|
|
44
|
+
request = request.model_copy(update={'project_agent_id': agent['agent_id']})
|
|
44
45
|
return await self._commit_episode(
|
|
45
46
|
space,
|
|
46
47
|
request.episode,
|
|
@@ -65,7 +66,7 @@ class StoreKnowledge:
|
|
|
65
66
|
self, actor, space, request.personal_project_id
|
|
66
67
|
)
|
|
67
68
|
if request.project_agent_id:
|
|
68
|
-
await authorize_project_agent(
|
|
69
|
+
agent = await authorize_project_agent(
|
|
69
70
|
self,
|
|
70
71
|
actor,
|
|
71
72
|
space,
|
|
@@ -74,6 +75,7 @@ class StoreKnowledge:
|
|
|
74
75
|
require_active=True,
|
|
75
76
|
require_memory=True,
|
|
76
77
|
)
|
|
78
|
+
request = request.model_copy(update={'project_agent_id': agent['agent_id']})
|
|
77
79
|
return await self._commit_episode(
|
|
78
80
|
space,
|
|
79
81
|
request.episode,
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
from fastapi import HTTPException
|
|
5
|
+
|
|
6
|
+
from .personal_project_access import authorize_personal_project
|
|
7
|
+
from .project_agent_access import authorize_project_agent
|
|
8
|
+
from .project_agent_attention_models import (
|
|
9
|
+
AgentAttentionCreate, AgentAttentionDecision, AgentAttentionList,
|
|
10
|
+
AgentAttentionRecord,
|
|
11
|
+
)
|
|
12
|
+
from .provider_values import native_datetime, now_utc, stable_uuid
|
|
13
|
+
from .system_hr_identity import resolve_hr_alias
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class StoreProjectAgentAttention:
|
|
17
|
+
"""Explicit human requests; never infer attention from worker/task status."""
|
|
18
|
+
|
|
19
|
+
async def create_agent_attention(self, actor, request: AgentAttentionCreate):
|
|
20
|
+
self._require_personal()
|
|
21
|
+
space = await self.authorize(actor, request.personal_space_id, 'maintainer')
|
|
22
|
+
agent_id = await resolve_hr_alias(self, request.personal_space_id, request.agent_id)
|
|
23
|
+
await authorize_project_agent(
|
|
24
|
+
self, actor, space, request.personal_project_id, agent_id,
|
|
25
|
+
require_active=True,
|
|
26
|
+
allow_unassigned=agent_id in {
|
|
27
|
+
'employee.bole', 'employee.jefa', 'fuli-project-coordinator',
|
|
28
|
+
},
|
|
29
|
+
)
|
|
30
|
+
if request.task_id:
|
|
31
|
+
task = await self._find_task_row(
|
|
32
|
+
request.personal_space_id, request.task_id,
|
|
33
|
+
request.personal_project_id,
|
|
34
|
+
)
|
|
35
|
+
if not task:
|
|
36
|
+
raise HTTPException(status_code=404, detail='project Agent task not found')
|
|
37
|
+
payload = request.model_dump(exclude={'idempotency_key'})
|
|
38
|
+
fingerprint = hashlib.sha256(json.dumps(payload, sort_keys=True).encode()).hexdigest()
|
|
39
|
+
payload['agent_id'] = agent_id
|
|
40
|
+
request_id = stable_uuid(
|
|
41
|
+
self.settings.provider_id, request.personal_space_id,
|
|
42
|
+
request.personal_project_id, 'agent-attention', request.idempotency_key,
|
|
43
|
+
)
|
|
44
|
+
timestamp = now_utc()
|
|
45
|
+
rows, _, _ = await self.runtime.driver.execute_query(
|
|
46
|
+
'''
|
|
47
|
+
MATCH (space:FuliSpace {id: $personal_space_id, kind: 'personal'})
|
|
48
|
+
MERGE (request:FuliAgentAttention {id: $request_id})
|
|
49
|
+
ON CREATE SET request += $payload,
|
|
50
|
+
request.request_id = $request_id,
|
|
51
|
+
request.fingerprint = $fingerprint,
|
|
52
|
+
request.status = 'open', request.revision = 0,
|
|
53
|
+
request.created_at = $timestamp,
|
|
54
|
+
request.updated_at = $timestamp
|
|
55
|
+
MERGE (space)-[:HAS_AGENT_ATTENTION]->(request)
|
|
56
|
+
RETURN request
|
|
57
|
+
''',
|
|
58
|
+
personal_space_id=request.personal_space_id, request_id=request_id,
|
|
59
|
+
payload=payload, fingerprint=fingerprint, timestamp=timestamp,
|
|
60
|
+
)
|
|
61
|
+
raw = dict(rows[0]['request'])
|
|
62
|
+
if raw['fingerprint'] != fingerprint:
|
|
63
|
+
raise HTTPException(status_code=409, detail='attention idempotency key conflict')
|
|
64
|
+
return attention_record(raw)
|
|
65
|
+
|
|
66
|
+
async def list_agent_attention(
|
|
67
|
+
self, actor, personal_space_id, personal_project_id=None,
|
|
68
|
+
agent_id=None, status='open', limit=100, offset=0,
|
|
69
|
+
):
|
|
70
|
+
self._require_personal()
|
|
71
|
+
space = await self.authorize(actor, personal_space_id, 'reader')
|
|
72
|
+
agent_id = await resolve_hr_alias(self, personal_space_id, agent_id)
|
|
73
|
+
if personal_project_id:
|
|
74
|
+
await authorize_personal_project(self, actor, space, personal_project_id)
|
|
75
|
+
rows, _, _ = await self.runtime.driver.execute_query(
|
|
76
|
+
'''
|
|
77
|
+
MATCH (:FuliSpace {id: $space_id, kind: 'personal'})-
|
|
78
|
+
[:HAS_AGENT_ATTENTION]->(request:FuliAgentAttention)
|
|
79
|
+
WHERE ($project_id IS NULL OR request.personal_project_id = $project_id)
|
|
80
|
+
AND ($agent_id IS NULL OR request.agent_id = $agent_id)
|
|
81
|
+
AND ($status IS NULL OR request.status = $status)
|
|
82
|
+
WITH request ORDER BY request.created_at DESC, request.request_id
|
|
83
|
+
WITH collect(request) AS requests
|
|
84
|
+
RETURN requests[$offset..($offset + $limit)] AS items,
|
|
85
|
+
size(requests) AS total,
|
|
86
|
+
[r IN requests WHERE r.status = 'open' | r.agent_id] AS open_agents
|
|
87
|
+
''', space_id=personal_space_id, project_id=personal_project_id,
|
|
88
|
+
agent_id=agent_id, status=status, limit=limit, offset=offset, routing_='r',
|
|
89
|
+
)
|
|
90
|
+
row = rows[0] if rows else {'items': [], 'total': 0, 'open_agents': []}
|
|
91
|
+
counts = {}
|
|
92
|
+
for agent in row['open_agents']:
|
|
93
|
+
counts[agent] = counts.get(agent, 0) + 1
|
|
94
|
+
return AgentAttentionList(
|
|
95
|
+
items=[attention_record(dict(item)) for item in row['items']],
|
|
96
|
+
total=row['total'], counts=counts,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
async def decide_agent_attention(
|
|
100
|
+
self, actor, request: AgentAttentionDecision, *, human: bool,
|
|
101
|
+
):
|
|
102
|
+
self._require_personal()
|
|
103
|
+
space = await self.authorize(actor, request.personal_space_id, 'maintainer')
|
|
104
|
+
await authorize_personal_project(self, actor, space, request.personal_project_id)
|
|
105
|
+
timestamp = now_utc()
|
|
106
|
+
# Increment obtains a write lock before the revision predicate is checked.
|
|
107
|
+
# A stale caller cannot overwrite the response recorded by another client.
|
|
108
|
+
rows, _, _ = await self.runtime.driver.execute_query(
|
|
109
|
+
'''
|
|
110
|
+
MATCH (:FuliSpace {id: $space_id, kind: 'personal'})-
|
|
111
|
+
[:HAS_AGENT_ATTENTION]->(request:FuliAgentAttention {
|
|
112
|
+
request_id: $request_id, personal_project_id: $project_id
|
|
113
|
+
})
|
|
114
|
+
SET request.lock_version = coalesce(request.lock_version, 0) + 1
|
|
115
|
+
WITH request
|
|
116
|
+
WHERE request.revision = $revision AND request.status = 'open'
|
|
117
|
+
SET request.status = $status, request.response = $response,
|
|
118
|
+
request.responded_by = $responded_by,
|
|
119
|
+
request.revision = request.revision + 1,
|
|
120
|
+
request.updated_at = $timestamp
|
|
121
|
+
RETURN request
|
|
122
|
+
''', space_id=request.personal_space_id,
|
|
123
|
+
project_id=request.personal_project_id, request_id=request.request_id,
|
|
124
|
+
revision=request.expected_revision, response=request.response,
|
|
125
|
+
status='resolved' if human else 'cancelled',
|
|
126
|
+
responded_by='human' if human else 'agent', timestamp=timestamp,
|
|
127
|
+
)
|
|
128
|
+
if not rows:
|
|
129
|
+
raise HTTPException(status_code=409, detail='attention changed or is no longer open; reload it')
|
|
130
|
+
return attention_record(dict(rows[0]['request']))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def attention_record(raw):
|
|
134
|
+
return AgentAttentionRecord(**{
|
|
135
|
+
key: native_datetime(raw.get(key)) if key in {'created_at', 'updated_at'} else raw.get(key)
|
|
136
|
+
for key in AgentAttentionRecord.model_fields
|
|
137
|
+
if key in raw
|
|
138
|
+
})
|
|
@@ -22,6 +22,7 @@ from .project_agent_executor_models import (
|
|
|
22
22
|
from .project_agent_models import ProjectAgentModelStrategy
|
|
23
23
|
from .provider_values import native_datetime, now_utc, stable_uuid
|
|
24
24
|
from .store_transactions import query_store_transaction
|
|
25
|
+
from .system_hr_identity import resolve_hr_alias
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
_TERMINAL_NEUTRAL = {'cancelled'}
|
|
@@ -276,6 +277,7 @@ class StoreProjectAgentExecutorLearning:
|
|
|
276
277
|
) -> list[ProjectAgentExecutorOutcomeEvidenceRecord]:
|
|
277
278
|
self._require_personal()
|
|
278
279
|
space = await self.authorize(actor, personal_space_id, 'reader')
|
|
280
|
+
agent_id = await resolve_hr_alias(self, personal_space_id, agent_id)
|
|
279
281
|
await authorize_personal_project(
|
|
280
282
|
self,
|
|
281
283
|
actor,
|
|
@@ -334,6 +336,7 @@ class StoreProjectAgentExecutorLearning:
|
|
|
334
336
|
|
|
335
337
|
self._require_personal()
|
|
336
338
|
space = await self.authorize(actor, personal_space_id, 'reader')
|
|
339
|
+
agent_id = await resolve_hr_alias(self, personal_space_id, agent_id)
|
|
337
340
|
if personal_project_id is not None:
|
|
338
341
|
await authorize_personal_project(
|
|
339
342
|
self,
|
|
@@ -367,12 +370,17 @@ class StoreProjectAgentExecutorLearning:
|
|
|
367
370
|
aggregates: list[ProjectAgentExecutorOutcomeAggregate] = []
|
|
368
371
|
for row in records:
|
|
369
372
|
raw = dict(row.get('aggregate') or {})
|
|
373
|
+
identity = raw.get('agent_id')
|
|
370
374
|
aggregate_json = raw.get('aggregate_json')
|
|
371
375
|
if aggregate_json:
|
|
372
376
|
try:
|
|
373
377
|
raw.update(json.loads(aggregate_json))
|
|
374
378
|
except (TypeError, ValueError):
|
|
375
379
|
pass
|
|
380
|
+
if raw.get('hr_identity_original_refs_json'):
|
|
381
|
+
raw['agent_id'] = identity
|
|
382
|
+
raw.pop('hr_identity_original_refs_json', None)
|
|
383
|
+
raw.pop('hr_identity_original_id', None)
|
|
376
384
|
# Storage identities are intentionally internal: the public model
|
|
377
385
|
# remains the provider-neutral bucket contract.
|
|
378
386
|
raw.pop('id', None)
|
|
@@ -16,6 +16,7 @@ from .project_agent_memory_models import (
|
|
|
16
16
|
ProjectAgentMemoryWrite,
|
|
17
17
|
)
|
|
18
18
|
from .provider_values import now_utc, stable_uuid
|
|
19
|
+
from .system_hr_identity import LEGACY_HR_AGENT_ID, SYSTEM_HR_AGENT_ID, resolve_hr_alias
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
class StoreProjectAgentMemory:
|
|
@@ -24,7 +25,7 @@ class StoreProjectAgentMemory:
|
|
|
24
25
|
) -> ProjectAgentMemoryView:
|
|
25
26
|
self._require_personal()
|
|
26
27
|
space = await self.authorize(actor, personal_space_id, 'reader')
|
|
27
|
-
await authorize_project_agent(
|
|
28
|
+
agent = await authorize_project_agent(
|
|
28
29
|
self, actor, space, personal_project_id, agent_id, require_memory=True,
|
|
29
30
|
)
|
|
30
31
|
records, _, _ = await self.runtime.driver.execute_query(
|
|
@@ -34,21 +35,21 @@ class StoreProjectAgentMemory:
|
|
|
34
35
|
RETURN checkpoint.record_json AS record_json
|
|
35
36
|
ORDER BY checkpoint.revision DESC LIMIT $limit
|
|
36
37
|
''',
|
|
37
|
-
memory_id=self.
|
|
38
|
-
personal_space_id, personal_project_id, agent_id,
|
|
39
|
-
),
|
|
38
|
+
memory_id=await self._resolved_agent_memory_id(
|
|
39
|
+
personal_space_id, personal_project_id, agent['agent_id']),
|
|
40
40
|
limit=max(1, min(limit, 10)),
|
|
41
41
|
routing_='r',
|
|
42
42
|
)
|
|
43
43
|
history = [
|
|
44
|
-
ProjectAgentMemoryRecord.model_validate_json(row['record_json'])
|
|
44
|
+
ProjectAgentMemoryRecord.model_validate_json(row['record_json']).model_copy(
|
|
45
|
+
update={'agent_id': agent['agent_id']})
|
|
45
46
|
for row in records
|
|
46
47
|
]
|
|
47
48
|
current = history[0] if history else None
|
|
48
49
|
return ProjectAgentMemoryView(
|
|
49
50
|
personal_space_id=personal_space_id,
|
|
50
51
|
personal_project_id=personal_project_id,
|
|
51
|
-
agent_id=agent_id,
|
|
52
|
+
agent_id=agent['agent_id'],
|
|
52
53
|
revision=current.revision if current else 0,
|
|
53
54
|
current=current,
|
|
54
55
|
history=history,
|
|
@@ -78,14 +79,14 @@ class StoreProjectAgentMemory:
|
|
|
78
79
|
''',
|
|
79
80
|
space_id=request.personal_space_id,
|
|
80
81
|
project_id=request.personal_project_id,
|
|
81
|
-
agent_id=
|
|
82
|
+
agent_id=agent['agent_id'],
|
|
82
83
|
task_id=request.task_id,
|
|
83
84
|
routing_='r',
|
|
84
85
|
)
|
|
85
86
|
if not task_rows:
|
|
86
87
|
raise HTTPException(status_code=404, detail='Agent memory task not found')
|
|
87
|
-
memory_id = self.
|
|
88
|
-
request.personal_space_id, request.personal_project_id,
|
|
88
|
+
memory_id = await self._resolved_agent_memory_id(
|
|
89
|
+
request.personal_space_id, request.personal_project_id, agent['agent_id'],
|
|
89
90
|
)
|
|
90
91
|
checkpoint_id = stable_uuid(memory_id, request.idempotency_key)
|
|
91
92
|
payload = request.model_dump(mode='json', exclude={
|
|
@@ -98,7 +99,7 @@ class StoreProjectAgentMemory:
|
|
|
98
99
|
checkpoint_id=checkpoint_id,
|
|
99
100
|
personal_space_id=request.personal_space_id,
|
|
100
101
|
personal_project_id=request.personal_project_id,
|
|
101
|
-
agent_id=
|
|
102
|
+
agent_id=agent['agent_id'],
|
|
102
103
|
revision=request.expected_revision + 1,
|
|
103
104
|
memory=request.memory,
|
|
104
105
|
source_application=request.source_application,
|
|
@@ -147,7 +148,7 @@ class StoreProjectAgentMemory:
|
|
|
147
148
|
''',
|
|
148
149
|
space_id=request.personal_space_id,
|
|
149
150
|
project_id=request.personal_project_id,
|
|
150
|
-
agent_id=
|
|
151
|
+
agent_id=agent['agent_id'],
|
|
151
152
|
memory_id=memory_id,
|
|
152
153
|
checkpoint_id=checkpoint_id,
|
|
153
154
|
expected_revision=request.expected_revision,
|
|
@@ -164,9 +165,35 @@ class StoreProjectAgentMemory:
|
|
|
164
165
|
status_code=409,
|
|
165
166
|
detail='Agent memory changed; reload its latest revision before merging notes',
|
|
166
167
|
)
|
|
167
|
-
|
|
168
|
+
compatible_hashes = {payload_hash}
|
|
169
|
+
if LEGACY_HR_AGENT_ID in agent.get('legacy_agent_ids', []):
|
|
170
|
+
# Old checkpoints keep their exact IDs/JSON/hashes. A canonical
|
|
171
|
+
# retry of the same input may differ only in the identity spelling.
|
|
172
|
+
for identity in (LEGACY_HR_AGENT_ID, SYSTEM_HR_AGENT_ID):
|
|
173
|
+
compatible_hashes.add(hashlib.sha256(json.dumps(
|
|
174
|
+
{**payload, 'agent_id': identity}, sort_keys=True,
|
|
175
|
+
ensure_ascii=False, separators=(',', ':'),
|
|
176
|
+
).encode()).hexdigest())
|
|
177
|
+
if row['payload_hash'] not in compatible_hashes:
|
|
168
178
|
raise HTTPException(status_code=409, detail='memory idempotency key has different input')
|
|
169
|
-
return ProjectAgentMemoryRecord.model_validate_json(row['record_json'])
|
|
179
|
+
return ProjectAgentMemoryRecord.model_validate_json(row['record_json']).model_copy(
|
|
180
|
+
update={'agent_id': agent['agent_id']})
|
|
181
|
+
|
|
182
|
+
async def _resolved_agent_memory_id(self, space_id, project_id, agent_id):
|
|
183
|
+
agent_id = await resolve_hr_alias(self, space_id, agent_id)
|
|
184
|
+
if agent_id == SYSTEM_HR_AGENT_ID:
|
|
185
|
+
rows, _, _ = await self.runtime.driver.execute_query(
|
|
186
|
+
'''MATCH (:FuliSpace {id: $space_id})-[:HAS_PROJECT_AGENT_IDENTITY]->
|
|
187
|
+
(:FuliProjectAgent {agent_id: $agent_id})-[:HAS_WORKING_MEMORY]->
|
|
188
|
+
(head:FuliProjectAgentMemory {personal_space_id: $space_id,
|
|
189
|
+
personal_project_id: $project_id})
|
|
190
|
+
RETURN DISTINCT head.id AS id''',
|
|
191
|
+
space_id=space_id, project_id=project_id, agent_id=agent_id, routing_='r')
|
|
192
|
+
if len(rows) > 1:
|
|
193
|
+
raise HTTPException(409, 'Agent has conflicting memory heads')
|
|
194
|
+
if rows:
|
|
195
|
+
return rows[0]['id']
|
|
196
|
+
return self._agent_memory_id(space_id, project_id, agent_id)
|
|
170
197
|
|
|
171
198
|
def _agent_memory_id(self, space_id, project_id, agent_id):
|
|
172
199
|
return stable_uuid(
|
|
@@ -24,6 +24,7 @@ from .project_agent_task_models import (
|
|
|
24
24
|
ProjectAgentRoutingDecisionRecord,
|
|
25
25
|
)
|
|
26
26
|
from .provider_values import native_datetime, now_utc, stable_uuid
|
|
27
|
+
from .store_project_agents import SYSTEM_HR_AGENT_ID
|
|
27
28
|
from .store_transactions import query_store_transaction
|
|
28
29
|
|
|
29
30
|
|
|
@@ -744,9 +745,11 @@ class StoreProjectAgentTaskRecruitment:
|
|
|
744
745
|
MATCH (:FuliSpace {id: $personal_space_id, kind: 'personal'})-
|
|
745
746
|
[:HAS_PROJECT_AGENT_IDENTITY]->
|
|
746
747
|
(hr:FuliProjectAgent {agent_type: 'hr', status: 'active'})
|
|
747
|
-
RETURN hr ORDER BY
|
|
748
|
+
RETURN hr ORDER BY CASE WHEN hr.agent_id = $system_hr_agent_id
|
|
749
|
+
THEN 0 ELSE 1 END, hr.created_at, hr.agent_id LIMIT 1
|
|
748
750
|
''',
|
|
749
751
|
personal_space_id=request.personal_space_id,
|
|
752
|
+
system_hr_agent_id=SYSTEM_HR_AGENT_ID,
|
|
750
753
|
routing_='r',
|
|
751
754
|
)
|
|
752
755
|
hr_agent_id = (
|
|
@@ -7,6 +7,7 @@ from fastapi import HTTPException
|
|
|
7
7
|
from .project_agent_access import authorize_project_agent
|
|
8
8
|
from .project_agent_models import ProjectAgentProfile
|
|
9
9
|
from .project_agent_task_models import ProjectAgentParallelPlan
|
|
10
|
+
from .store_project_agents import SYSTEM_HR_AGENT_ID
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class StoreProjectAgentTaskStaffing:
|
|
@@ -634,9 +635,11 @@ class StoreProjectAgentTaskStaffing:
|
|
|
634
635
|
MATCH (space:FuliSpace {id: $personal_space_id, kind: 'personal'})-
|
|
635
636
|
[:HAS_PROJECT_AGENT_IDENTITY]->
|
|
636
637
|
(hr:FuliProjectAgent {agent_type: 'hr', status: 'active'})
|
|
637
|
-
RETURN hr
|
|
638
|
+
RETURN hr ORDER BY CASE WHEN hr.agent_id = $system_hr_agent_id
|
|
639
|
+
THEN 0 ELSE 1 END, hr.created_at, hr.agent_id LIMIT 1
|
|
638
640
|
''',
|
|
639
641
|
personal_space_id=request.personal_space_id,
|
|
642
|
+
system_hr_agent_id=SYSTEM_HR_AGENT_ID,
|
|
640
643
|
routing_='r',
|
|
641
644
|
)
|
|
642
645
|
if not hr_records:
|
|
@@ -2,6 +2,7 @@ import json
|
|
|
2
2
|
from collections import defaultdict
|
|
3
3
|
from datetime import date
|
|
4
4
|
from fastapi import HTTPException
|
|
5
|
+
from .system_hr_identity import resolve_hr_alias
|
|
5
6
|
from .personal_project_access import authorize_personal_project
|
|
6
7
|
from .project_agent_access import authorize_project_agent
|
|
7
8
|
from .project_agent_models import ProjectAgentModelStrategy, ProjectAgentProfile
|
|
@@ -470,6 +471,7 @@ class StoreProjectAgentTasks(
|
|
|
470
471
|
) -> list[ProjectAgentTaskRecord]:
|
|
471
472
|
self._require_personal()
|
|
472
473
|
space = await self.authorize(actor, personal_space_id, 'reader')
|
|
474
|
+
agent_id = await resolve_hr_alias(self, personal_space_id, agent_id)
|
|
473
475
|
if personal_project_id:
|
|
474
476
|
await authorize_personal_project(
|
|
475
477
|
self,
|
|
@@ -510,6 +512,7 @@ class StoreProjectAgentTasks(
|
|
|
510
512
|
) -> ProjectAgentActivityResult:
|
|
511
513
|
self._require_personal()
|
|
512
514
|
space = await self.authorize(actor, personal_space_id, 'reader')
|
|
515
|
+
agent_id = await resolve_hr_alias(self, personal_space_id, agent_id)
|
|
513
516
|
if personal_project_id:
|
|
514
517
|
await authorize_personal_project(
|
|
515
518
|
self,
|
|
@@ -18,12 +18,87 @@ from .project_agent_models import (
|
|
|
18
18
|
ProjectAgentUpsert,
|
|
19
19
|
)
|
|
20
20
|
from .provider_values import native_datetime, now_utc, stable_uuid
|
|
21
|
+
from .project_agent_names import agent_display_name
|
|
22
|
+
from .store_transactions import query_store_transaction
|
|
23
|
+
from .system_hr_identity import LEGACY_HR_AGENT_ID, merge_legacy_hr, resolve_hr_alias
|
|
21
24
|
|
|
22
25
|
|
|
23
26
|
SYSTEM_COORDINATOR_AGENT_ID = 'fuli-project-coordinator'
|
|
27
|
+
SYSTEM_HR_AGENT_ID = 'employee.bole'
|
|
24
28
|
|
|
25
29
|
|
|
26
30
|
class StoreProjectAgents:
|
|
31
|
+
async def ensure_system_project_hr(
|
|
32
|
+
self,
|
|
33
|
+
actor: dict,
|
|
34
|
+
personal_space_id: str,
|
|
35
|
+
) -> ProjectAgentRecord:
|
|
36
|
+
self._require_personal()
|
|
37
|
+
await self.authorize(actor, personal_space_id, 'maintainer')
|
|
38
|
+
async with query_store_transaction(self) as scoped:
|
|
39
|
+
await scoped._ensure_system_project_hr_identity(personal_space_id)
|
|
40
|
+
await merge_legacy_hr(scoped, personal_space_id)
|
|
41
|
+
return await self.get_project_agent(actor, personal_space_id, None, SYSTEM_HR_AGENT_ID)
|
|
42
|
+
|
|
43
|
+
async def _ensure_system_project_hr_identity(self, personal_space_id):
|
|
44
|
+
profile = ProjectAgentProfile(
|
|
45
|
+
name='Bole',
|
|
46
|
+
occupation_emoji='🔎',
|
|
47
|
+
responsibility=(
|
|
48
|
+
'维护 Agent 人员分布、当前工作与可审计招募记录,'
|
|
49
|
+
'并在需要新角色时执行受策略约束的招募。'
|
|
50
|
+
),
|
|
51
|
+
agent_type='hr',
|
|
52
|
+
work_kinds=['agent-recruitment', 'staffing-review'],
|
|
53
|
+
capabilities=[
|
|
54
|
+
'Agent 招募', '人员分布', '工作状态', '招募审计',
|
|
55
|
+
'fuli.employee:bole',
|
|
56
|
+
],
|
|
57
|
+
initial_preferences=[
|
|
58
|
+
'每次招募保留任务、原因、触发来源与时间线。',
|
|
59
|
+
'只展示 Provider 已记录的人员状态,不推测未上报的工作。',
|
|
60
|
+
],
|
|
61
|
+
status='active',
|
|
62
|
+
)
|
|
63
|
+
updated_at = now_utc()
|
|
64
|
+
node_id = stable_uuid(
|
|
65
|
+
self.settings.provider_id,
|
|
66
|
+
personal_space_id,
|
|
67
|
+
'project-agent',
|
|
68
|
+
SYSTEM_HR_AGENT_ID,
|
|
69
|
+
)
|
|
70
|
+
await self.runtime.driver.execute_query(
|
|
71
|
+
'''
|
|
72
|
+
MATCH (space:FuliSpace {id: $personal_space_id, kind: 'personal'})
|
|
73
|
+
MERGE (agent:FuliProjectAgent {id: $id})
|
|
74
|
+
ON CREATE SET agent.agent_id = $agent_id,
|
|
75
|
+
agent.created_at = $updated_at,
|
|
76
|
+
agent.system_managed = true
|
|
77
|
+
ON CREATE SET agent.profile_json = $profile_json,
|
|
78
|
+
agent.name = $name,
|
|
79
|
+
agent.occupation_emoji = $occupation_emoji,
|
|
80
|
+
agent.responsibility = $responsibility,
|
|
81
|
+
agent.capabilities = $capabilities,
|
|
82
|
+
agent.work_kinds = $work_kinds,
|
|
83
|
+
agent.agent_type = 'hr',
|
|
84
|
+
agent.memory_scope = 'reviewed_agent',
|
|
85
|
+
agent.status = 'active',
|
|
86
|
+
agent.updated_at = $updated_at
|
|
87
|
+
MERGE (space)-[:HAS_PROJECT_AGENT_IDENTITY]->(agent)
|
|
88
|
+
RETURN agent
|
|
89
|
+
''',
|
|
90
|
+
personal_space_id=personal_space_id,
|
|
91
|
+
id=node_id,
|
|
92
|
+
agent_id=SYSTEM_HR_AGENT_ID,
|
|
93
|
+
profile_json=profile.model_dump_json(),
|
|
94
|
+
name=profile.name,
|
|
95
|
+
occupation_emoji=profile.occupation_emoji,
|
|
96
|
+
responsibility=profile.responsibility,
|
|
97
|
+
capabilities=profile.capabilities,
|
|
98
|
+
work_kinds=profile.work_kinds,
|
|
99
|
+
updated_at=updated_at,
|
|
100
|
+
)
|
|
101
|
+
|
|
27
102
|
async def ensure_system_project_coordinator(
|
|
28
103
|
self,
|
|
29
104
|
actor: dict,
|
|
@@ -93,6 +168,8 @@ class StoreProjectAgents:
|
|
|
93
168
|
recruitment_id: str | None = None,
|
|
94
169
|
) -> ProjectAgentRecord:
|
|
95
170
|
self._require_personal()
|
|
171
|
+
if request.agent_id == LEGACY_HR_AGENT_ID:
|
|
172
|
+
raise HTTPException(409, 'This HR identity has moved to employee.bole')
|
|
96
173
|
if request.profile.agent_type == 'temporary' and not recruitment_id:
|
|
97
174
|
raise HTTPException(
|
|
98
175
|
status_code=422,
|
|
@@ -106,6 +183,14 @@ class StoreProjectAgents:
|
|
|
106
183
|
status_code=422,
|
|
107
184
|
detail='only the system-managed identity may use coordinator type',
|
|
108
185
|
)
|
|
186
|
+
if (
|
|
187
|
+
request.profile.agent_type == 'hr'
|
|
188
|
+
and request.agent_id != SYSTEM_HR_AGENT_ID
|
|
189
|
+
):
|
|
190
|
+
raise HTTPException(
|
|
191
|
+
status_code=422,
|
|
192
|
+
detail='only the system-managed Bole identity may use HR type',
|
|
193
|
+
)
|
|
109
194
|
if (
|
|
110
195
|
request.agent_id == SYSTEM_COORDINATOR_AGENT_ID
|
|
111
196
|
and request.profile.agent_type != 'coordinator'
|
|
@@ -114,6 +199,19 @@ class StoreProjectAgents:
|
|
|
114
199
|
status_code=422,
|
|
115
200
|
detail='the system coordinator identity cannot change Agent type',
|
|
116
201
|
)
|
|
202
|
+
if (
|
|
203
|
+
request.agent_id == SYSTEM_HR_AGENT_ID
|
|
204
|
+
and request.profile.agent_type != 'hr'
|
|
205
|
+
):
|
|
206
|
+
raise HTTPException(
|
|
207
|
+
status_code=422,
|
|
208
|
+
detail='the system HR identity cannot change Agent type',
|
|
209
|
+
)
|
|
210
|
+
if request.agent_id == SYSTEM_HR_AGENT_ID and request.profile.status != 'active':
|
|
211
|
+
raise HTTPException(
|
|
212
|
+
status_code=422,
|
|
213
|
+
detail='the system HR must remain active',
|
|
214
|
+
)
|
|
117
215
|
space = await self.authorize(actor, request.personal_space_id, 'maintainer')
|
|
118
216
|
if request.personal_project_id:
|
|
119
217
|
await authorize_personal_project(
|
|
@@ -344,6 +442,7 @@ class StoreProjectAgents:
|
|
|
344
442
|
) -> ProjectAgentRecord:
|
|
345
443
|
self._require_personal()
|
|
346
444
|
space = await self.authorize(actor, personal_space_id, 'reader')
|
|
445
|
+
agent_id = await resolve_hr_alias(self, personal_space_id, agent_id)
|
|
347
446
|
if personal_project_id:
|
|
348
447
|
await authorize_personal_project(
|
|
349
448
|
self,
|
|
@@ -391,6 +490,11 @@ class StoreProjectAgents:
|
|
|
391
490
|
status_code=422,
|
|
392
491
|
detail='the system coordinator cannot be archived',
|
|
393
492
|
)
|
|
493
|
+
if agent_id == SYSTEM_HR_AGENT_ID:
|
|
494
|
+
raise HTTPException(
|
|
495
|
+
status_code=422,
|
|
496
|
+
detail='the system HR cannot be archived',
|
|
497
|
+
)
|
|
394
498
|
await self.authorize(actor, personal_space_id, 'maintainer')
|
|
395
499
|
if not reason.strip():
|
|
396
500
|
raise HTTPException(status_code=422, detail='archive reason is required')
|
|
@@ -696,6 +800,7 @@ class StoreProjectAgents:
|
|
|
696
800
|
) -> list[ProjectAgentAssignmentRecord]:
|
|
697
801
|
self._require_personal()
|
|
698
802
|
space = await self.authorize(actor, personal_space_id, 'reader')
|
|
803
|
+
agent_id = await resolve_hr_alias(self, personal_space_id, agent_id)
|
|
699
804
|
if personal_project_id:
|
|
700
805
|
await authorize_personal_project(
|
|
701
806
|
self,
|
|
@@ -974,6 +1079,9 @@ class StoreProjectAgents:
|
|
|
974
1079
|
) -> ProjectAgentRecord:
|
|
975
1080
|
raw = dict(row['agent'])
|
|
976
1081
|
profile = ProjectAgentProfile.model_validate_json(raw['profile_json'])
|
|
1082
|
+
profile = profile.model_copy(update={
|
|
1083
|
+
'display_name': agent_display_name(raw['agent_id'], profile.name, profile.display_name),
|
|
1084
|
+
})
|
|
977
1085
|
# ``occupation_emoji`` was introduced after the original profile JSON
|
|
978
1086
|
# contract. Prefer the durable profile value, but tolerate an older
|
|
979
1087
|
# JSON payload while the explicit node property is being backfilled.
|
|
@@ -1066,6 +1174,7 @@ class StoreProjectAgents:
|
|
|
1066
1174
|
})
|
|
1067
1175
|
return ProjectAgentRecord(
|
|
1068
1176
|
agent_id=raw['agent_id'],
|
|
1177
|
+
legacy_agent_ids=list(raw.get('legacy_agent_ids') or []),
|
|
1069
1178
|
personal_space_id=personal_space_id,
|
|
1070
1179
|
personal_project_id=projection_project_id,
|
|
1071
1180
|
profile=profile,
|
|
@@ -143,7 +143,7 @@ class StoreTaskContexts:
|
|
|
143
143
|
# The existing checkpoint lets pre-upgrade claims retry without new writes.
|
|
144
144
|
memory_checkpoint_id = None
|
|
145
145
|
if record['project_agent_id'] and record['personal_project_id']:
|
|
146
|
-
memory_checkpoint_id = stable_uuid(self.
|
|
146
|
+
memory_checkpoint_id = stable_uuid(await self._resolved_agent_memory_id(
|
|
147
147
|
request.personal_space_id, record['personal_project_id'],
|
|
148
148
|
record['project_agent_id'],
|
|
149
149
|
), f'{token}:memory')
|