fuli-context 0.11.0 → 0.12.0
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/dist/web/assets/AboutPage-BAD0iPVX.js +1 -0
- package/dist/web/assets/AgentConversations-1iGTd4sz.js +2 -0
- package/dist/web/assets/AgentHand-D_RafKOM.js +1 -0
- package/dist/web/assets/AgentName-B5LKQ09q.js +1 -0
- package/dist/web/assets/AgentProfilePage-CioOdkBM.js +1 -0
- package/dist/web/assets/AgentsDirectoryPage--1A9nvNA.js +1 -0
- package/dist/web/assets/AgentsDirectoryPage-C7OUQH9v.css +1 -0
- package/dist/web/assets/ConnectionsPage-Tr2rRVOZ.js +1 -0
- package/dist/web/assets/EmployeeRecruitDialog-1QevD4Qv.js +1 -0
- package/dist/web/assets/EmployeeWorkbenchPage-B0vQIgwA.js +3 -0
- package/dist/web/assets/EmployeeWorkbenchPage-BQes4L1f.css +1 -0
- package/dist/web/assets/KnowledgeEditDialog-ClnrfPZi.js +1 -0
- package/dist/web/assets/KnowledgeInspector-Te1hJZbx.js +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-CCCrWl6Y.js +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-ccgscrPd.css +1 -0
- package/dist/web/assets/KnowledgePage-DGOcBwr8.js +1 -0
- package/dist/web/assets/KnowledgeWorkspace-cKn1NCRe.js +4 -0
- package/dist/web/assets/NotFoundPage-fetSnp0N.js +1 -0
- package/dist/web/assets/OverviewPage-BD4yY2Xz.js +1 -0
- package/dist/web/assets/PersonalProfilePage-DGJVP_VB.js +1 -0
- package/dist/web/assets/PersonalProjectsPage-D25HYMgD.js +1 -0
- package/dist/web/assets/ProjectAgentFirstTask-BcxqmKXQ.js +1 -0
- package/dist/web/assets/ProjectAgentsPage-BWDJBESb.js +12 -0
- package/dist/web/assets/{ProjectAgentsPage-DNLNjYp-.css → ProjectAgentsPage-DWGkgIsr.css} +1 -1
- package/dist/web/assets/ProjectScopePicker-yodM7lF_.js +1 -0
- package/dist/web/assets/PublicProjectsPage-CyNxVzW3.js +1 -0
- package/dist/web/assets/ReviewPage-Cy0Rt4CO.js +1 -0
- package/dist/web/assets/RoundtablesPage-BjVhvwMc.js +1 -0
- package/dist/web/assets/SettingsPage-CCZqZtnH.js +1 -0
- package/dist/web/assets/SettingsPage-DYz5bTwa.css +1 -0
- package/dist/web/assets/SharedJefaReportPage-C56kPciB.js +1 -0
- package/dist/web/assets/UiDialog-CvDxg3B1.js +1 -0
- package/dist/web/assets/UiDisclosure-D0iMTplF.js +1 -0
- package/dist/web/assets/UiSegmented-DIJW6wv3.js +1 -0
- package/dist/web/assets/UiSelect-CMR0QqF6.js +1 -0
- package/dist/web/assets/WritingTastePage-QgNzssfF.js +1 -0
- package/dist/web/assets/agent-pins-BcJ_qr8n.js +1 -0
- package/dist/web/assets/agent-work-summary-C-yW7x7A.js +1 -0
- package/dist/web/assets/{attention-store-BfxtL9EV.js → attention-store-B1DI8wmd.js} +1 -1
- package/dist/web/assets/{catalog-6SN9i3WR.js → catalog-DbXv0_bR.js} +1 -1
- package/dist/web/assets/client-CR-W5Wzp.js +1 -0
- package/dist/web/assets/console-D7GHOMEC.js +1 -0
- package/dist/web/assets/conversation-launcher-settings-Dj2GMzCI.js +1 -0
- package/dist/web/assets/{i18n-CCMR4gDS.js → i18n-CVFhT_k7.js} +3 -3
- package/dist/web/assets/{identity-C1eN-FSB.js → identity-CM6Dmy00.js} +1 -1
- package/dist/web/assets/index-CJ6Jmok9.css +1 -0
- package/dist/web/assets/index-CRa3GnYi.js +5 -0
- package/dist/web/assets/judgment-types-CJBEyJs1.js +1 -0
- package/dist/web/assets/model-WFhQZ3v_.js +1 -0
- package/dist/web/assets/{personal-profile-graph-BD1Y6TDS.js → personal-profile-graph-BwCh2M9G.js} +1 -1
- package/dist/web/assets/shared-report-CeIvrL9a.js +1 -0
- package/dist/web/assets/{agent-work-summary-LeW-Al_S.js → task-evidence-DTiPCs7r.js} +1 -1
- package/dist/web/assets/useAgentRoster-DCArHlxK.js +1 -0
- package/dist/web/assets/{useApi-CROJJdhE-DlOiu8lP.js → useApi-CROJJdhE-BYhbjaZB.js} +1 -1
- package/dist/web/assets/vue-router-DaxrQwX3.js +1 -0
- package/dist/web/index.html +16 -12
- package/docs/agent-roundtable.md +17 -1
- package/docs/employee-agents.md +1 -1
- package/docs/judgment-and-autonomy.md +33 -0
- package/graph-provider/fuli_graph/ai_review.py +154 -0
- package/graph-provider/fuli_graph/common_knowledge.py +12 -0
- package/graph-provider/fuli_graph/graph_models.py +14 -0
- package/graph-provider/fuli_graph/graph_query.py +217 -0
- package/graph-provider/fuli_graph/knowledge_audit.py +6 -0
- package/graph-provider/fuli_graph/knowledge_batch_confirmation.py +6 -0
- package/graph-provider/fuli_graph/knowledge_management.py +23 -0
- package/graph-provider/fuli_graph/knowledge_review.py +383 -4
- package/graph-provider/fuli_graph/knowledge_review_models.py +61 -2
- package/graph-provider/fuli_graph/personal_global_preferences.py +5 -1
- package/graph-provider/fuli_graph/preference_conflicts.py +91 -1
- package/graph-provider/fuli_graph/project_agent_context_models.py +5 -1
- package/graph-provider/fuli_graph/project_agent_delegations.py +118 -0
- package/graph-provider/fuli_graph/project_agent_employee_numbers.py +41 -0
- package/graph-provider/fuli_graph/project_agent_memory_routes.py +13 -0
- package/graph-provider/fuli_graph/project_agent_models.py +1 -0
- package/graph-provider/fuli_graph/project_agent_task_models.py +2 -0
- package/graph-provider/fuli_graph/project_agent_team_growth.py +2 -2
- package/graph-provider/fuli_graph/project_knowledge.py +41 -0
- package/graph-provider/fuli_graph/runtime.py +15 -0
- package/graph-provider/fuli_graph/store_agent_verification.py +12 -5
- package/graph-provider/fuli_graph/store_knowledge.py +98 -58
- package/graph-provider/fuli_graph/store_project_agent_attention.py +1 -1
- package/graph-provider/fuli_graph/store_project_agent_context.py +52 -4
- package/graph-provider/fuli_graph/store_project_agent_coordination_policy.py +16 -1
- package/graph-provider/fuli_graph/store_project_agent_task_activity.py +26 -0
- package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +5 -3
- package/graph-provider/fuli_graph/store_project_agents.py +31 -5
- package/graph-provider/fuli_graph/system_agents.py +23 -2
- package/graph-provider/fuli_graph/system_hr_identity.py +19 -5
- package/graph-provider/pyproject.toml +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +3 -2
- package/src/agent-roundtable/service.js +34 -5
- package/src/agent-roundtable/wake.js +2 -1
- package/src/agent-tools/graph-definitions.js +2 -0
- package/src/agent-tools/graph-handlers.js +7 -0
- package/src/agent-tools/interface-catalog.js +6 -0
- package/src/agent-tools/judgment-definitions.js +12 -0
- package/src/agents/claude-code/wake.js +7 -0
- package/src/agents/codex/wake.js +7 -0
- package/src/agents/identity-receipt.js +20 -2
- package/src/employees/catalog/tonborg.json +20 -0
- package/src/employees/manifest.js +2 -2
- package/src/employees/recruitment.js +20 -1
- package/src/graphiti/agent-collaboration.js +8 -2
- package/src/graphiti/federated-application.js +11 -2
- package/src/graphiti/knowledge-review.js +7 -3
- package/src/graphiti/project-agent-mapping.js +4 -0
- package/src/graphiti/project-agent-task-entry.js +23 -5
- package/src/graphiti/project-agent-team-context.js +4 -1
- package/src/graphiti/provider-client.js +9 -0
- package/src/graphiti/task-entry-preferences.js +17 -4
- package/src/graphiti/task-entry-project-scope.js +67 -0
- package/src/http/api-router.js +2 -0
- package/src/judgment/acceptance.js +91 -0
- package/src/judgment/engine.js +168 -0
- package/src/judgment/http.js +24 -0
- package/src/judgment/model.js +24 -0
- package/src/judgment/routing.js +96 -0
- package/src/judgment/scheduler.js +25 -0
- package/src/judgment/service.js +66 -0
- package/src/judgment/store.js +123 -0
- package/src/mcp/session-id.js +1 -0
- package/src/mcp/tool-annotations.js +4 -2
- package/src/server.js +9 -2
- package/dist/web/assets/AboutPage-CKq4IRHd.js +0 -1
- package/dist/web/assets/AgentConversations-DfXiFvBN.js +0 -2
- package/dist/web/assets/AgentHand-BBM-sKh2.js +0 -1
- package/dist/web/assets/AgentName-C_6R80nP.js +0 -1
- package/dist/web/assets/AgentProfilePage-Cz-i1fRD.js +0 -1
- package/dist/web/assets/AgentsDirectoryPage-BI0EFz6y.js +0 -1
- package/dist/web/assets/AgentsDirectoryPage-yRWktiin.css +0 -1
- package/dist/web/assets/ConnectionsPage-BoC6kXgs.js +0 -1
- package/dist/web/assets/EmployeeRecruitDialog-BVyclQAJ.js +0 -1
- package/dist/web/assets/EmployeeWorkbenchPage-D-c_Jm2z.css +0 -1
- package/dist/web/assets/EmployeeWorkbenchPage-D2GvYraI.js +0 -3
- package/dist/web/assets/KnowledgeEditDialog-a0AKWics.js +0 -1
- package/dist/web/assets/KnowledgeInspector-DMg9yJZ2.js +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-BgsHM3Qw.js +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-DeL7Jp34.css +0 -1
- package/dist/web/assets/KnowledgePage-kn8MqUCO.js +0 -1
- package/dist/web/assets/KnowledgeWorkspace-BtRq9fvC.js +0 -4
- package/dist/web/assets/NotFoundPage-D-7jLz3o.js +0 -1
- package/dist/web/assets/OverviewPage-BHPc-XxY.js +0 -1
- package/dist/web/assets/PersonalProfilePage-C00iV0Kx.js +0 -1
- package/dist/web/assets/PersonalProjectsPage-CFNkdHSo.js +0 -1
- package/dist/web/assets/ProjectAgentFirstTask-CrCWEYgn.js +0 -1
- package/dist/web/assets/ProjectAgentsPage-BzztLMH-.js +0 -12
- package/dist/web/assets/ProjectScopePicker-CMxn3txv.js +0 -1
- package/dist/web/assets/PublicProjectsPage-DQVtvhOS.js +0 -1
- package/dist/web/assets/ReviewPage-BZhvyDWc.js +0 -1
- package/dist/web/assets/RoundtablesPage-CVebTXQa.js +0 -1
- package/dist/web/assets/SettingsPage-1knvfOW6.js +0 -1
- package/dist/web/assets/SettingsPage-BmYkLul4.css +0 -1
- package/dist/web/assets/SharedJefaReportPage-BCVq8u0P.js +0 -1
- package/dist/web/assets/UiDialog-Da1P10f4.js +0 -1
- package/dist/web/assets/UiDisclosure-8wE8YuE4.js +0 -1
- package/dist/web/assets/UiSegmented-BKZXc8QF.js +0 -1
- package/dist/web/assets/UiSelect-D5Xj5Ec4.js +0 -1
- package/dist/web/assets/WritingTastePage-B8-etOBn.js +0 -1
- package/dist/web/assets/client-CUTkF3t4.js +0 -1
- package/dist/web/assets/console-CG8tdtSs.js +0 -1
- package/dist/web/assets/conversation-launcher-settings-CyZkLJg7.js +0 -1
- package/dist/web/assets/index-kPB0Cf8Q.css +0 -1
- package/dist/web/assets/index-sLDKI6R-.js +0 -5
- package/dist/web/assets/model-l-sAqtW_.js +0 -1
- package/dist/web/assets/shared-report-B46dxlCR.js +0 -1
- package/dist/web/assets/useAgentRoster-DdxoKDEE.js +0 -1
- package/dist/web/assets/vue-router-DjqEU4W2.js +0 -1
package/dist/web/index.html
CHANGED
|
@@ -9,24 +9,28 @@
|
|
|
9
9
|
/>
|
|
10
10
|
<link rel="icon" type="image/png" href="/assets/fuli-logo-ER1wQ8Kk.png" />
|
|
11
11
|
<title>复利 · Context Graph</title>
|
|
12
|
-
<script type="module" crossorigin src="/assets/index-
|
|
13
|
-
<link rel="modulepreload" crossorigin href="/assets/i18n-
|
|
12
|
+
<script type="module" crossorigin src="/assets/index-CRa3GnYi.js"></script>
|
|
13
|
+
<link rel="modulepreload" crossorigin href="/assets/i18n-CVFhT_k7.js">
|
|
14
14
|
<link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-BDNMzG2s.js">
|
|
15
|
-
<link rel="modulepreload" crossorigin href="/assets/client-
|
|
16
|
-
<link rel="modulepreload" crossorigin href="/assets/console-
|
|
17
|
-
<link rel="modulepreload" crossorigin href="/assets/UiDialog-
|
|
18
|
-
<link rel="modulepreload" crossorigin href="/assets/useApi-CROJJdhE-
|
|
19
|
-
<link rel="modulepreload" crossorigin href="/assets/vue-router-
|
|
15
|
+
<link rel="modulepreload" crossorigin href="/assets/client-CR-W5Wzp.js">
|
|
16
|
+
<link rel="modulepreload" crossorigin href="/assets/console-D7GHOMEC.js">
|
|
17
|
+
<link rel="modulepreload" crossorigin href="/assets/UiDialog-CvDxg3B1.js">
|
|
18
|
+
<link rel="modulepreload" crossorigin href="/assets/useApi-CROJJdhE-BYhbjaZB.js">
|
|
19
|
+
<link rel="modulepreload" crossorigin href="/assets/vue-router-DaxrQwX3.js">
|
|
20
20
|
<link rel="modulepreload" crossorigin href="/assets/fuli-logo-Bu9WBuQe.js">
|
|
21
|
-
<link rel="modulepreload" crossorigin href="/assets/UiSelect-
|
|
22
|
-
<link rel="modulepreload" crossorigin href="/assets/attention-store-
|
|
23
|
-
<link rel="modulepreload" crossorigin href="/assets/AgentHand-
|
|
24
|
-
<link rel="modulepreload" crossorigin href="/assets/UiDisclosure-
|
|
21
|
+
<link rel="modulepreload" crossorigin href="/assets/UiSelect-CMR0QqF6.js">
|
|
22
|
+
<link rel="modulepreload" crossorigin href="/assets/attention-store-B1DI8wmd.js">
|
|
23
|
+
<link rel="modulepreload" crossorigin href="/assets/AgentHand-D_RafKOM.js">
|
|
24
|
+
<link rel="modulepreload" crossorigin href="/assets/UiDisclosure-D0iMTplF.js">
|
|
25
|
+
<link rel="modulepreload" crossorigin href="/assets/profile-model-BQO4zNs9.js">
|
|
26
|
+
<link rel="modulepreload" crossorigin href="/assets/task-evidence-DTiPCs7r.js">
|
|
27
|
+
<link rel="modulepreload" crossorigin href="/assets/useAgentRoster-DCArHlxK.js">
|
|
28
|
+
<link rel="modulepreload" crossorigin href="/assets/agent-pins-BcJ_qr8n.js">
|
|
25
29
|
<link rel="modulepreload" crossorigin href="/assets/paths-CDREouhi.js">
|
|
26
30
|
<link rel="stylesheet" crossorigin href="/assets/client-CIuQ1icy.css">
|
|
27
31
|
<link rel="stylesheet" crossorigin href="/assets/UiSelect-Bv6R0yr1.css">
|
|
28
32
|
<link rel="stylesheet" crossorigin href="/assets/AgentHand-Y83Oo36U.css">
|
|
29
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
33
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CJ6Jmok9.css">
|
|
30
34
|
</head>
|
|
31
35
|
<body>
|
|
32
36
|
<div id="app"></div>
|
package/docs/agent-roundtable.md
CHANGED
|
@@ -18,11 +18,27 @@ All tools are on the normal `fuli` MCP server.
|
|
|
18
18
|
| `reply_agent_message` | Answer one of them |
|
|
19
19
|
| `read_agent_thread` | Every message of one thread |
|
|
20
20
|
|
|
21
|
-
`message_agent` takes `to` (Agent ID
|
|
21
|
+
`message_agent` takes `to` (Agent ID, exact name, or employee number such as
|
|
22
|
+
`FLA 000001`), `body`, the caller's
|
|
22
23
|
`taskContextToken`, `projectPath` (the current directory), and optionally `threadId`
|
|
23
24
|
to continue a thread or `conversation` to choose which of the recipient's
|
|
24
25
|
conversations answers (`auto` by default, `new`, or a session ID from `find_agents`).
|
|
25
26
|
|
|
27
|
+
Each Fuli Agent (FLA) has a permanent employee number within its personal space.
|
|
28
|
+
Numbers start at `000001`, expand beyond six digits when needed, and are never
|
|
29
|
+
reused after archival. Moving between projects or clients preserves the number;
|
|
30
|
+
existing IDs and profile links remain valid. `find_agents` can search by number.
|
|
31
|
+
|
|
32
|
+
Every project has one accountable lead. User requests for a member go through
|
|
33
|
+
that lead. A recorded lead-to-member roundtable delivery restores the member's
|
|
34
|
+
own isolated context and returns its answer to the lead for the final report.
|
|
35
|
+
Changing, deactivating, or archiving a project lead requires a handoff first.
|
|
36
|
+
Delivery requires an active task context. The Provider issues a short-lived
|
|
37
|
+
capability that Fuli passes only to the receiving process, binds to its session,
|
|
38
|
+
and revokes after each attempt. Public message headers do not grant access.
|
|
39
|
+
An answer is attributed to the recipient only after that session redeems the
|
|
40
|
+
capability. Members send their results through the project lead.
|
|
41
|
+
|
|
26
42
|
## Delivery
|
|
27
43
|
|
|
28
44
|
1. **Wake.** By default Fuli wakes the recipient in its own client, headlessly and
|
package/docs/employee-agents.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Specialist Agents
|
|
2
2
|
|
|
3
|
-
An employee template is a reusable role with an optional FULI-hosted or native workbench. Recruitment reuses the existing durable Project Agent identity and assignment model.
|
|
3
|
+
An employee template is a reusable role with an optional FULI-hosted or native workbench. Recruitment reuses the existing durable Project Agent identity and assignment model. Three roles are fixed: **Jefa**, the project manager, **Bole**, the HR Agent, and **Tonborg**, the judgment Agent. They exist in every personal space from the start, manage all projects by default, and are never offered for hire. They require no separate runtime package. See [judgment and autonomy](judgment-and-autonomy.md) for Tonborg's review, feedback, and execution-choice workflow.
|
|
4
4
|
|
|
5
5
|
## User flow
|
|
6
6
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Tonborg and autonomy
|
|
2
|
+
|
|
3
|
+
Tonborg is the built-in judgment Agent, alongside Jefa and Bole. It has a persistent FLA identity, scoped Agent memory, and a native decision workbench. All Agents can be pinned from their card; pins appear in navigation. The three built-in roles are the initial defaults. Removing a pin, including every pin, survives reloads.
|
|
4
|
+
|
|
5
|
+
## Choose the amount of autonomy
|
|
6
|
+
|
|
7
|
+
Settings offers three choices, saved immediately with Undo:
|
|
8
|
+
|
|
9
|
+
- **I decide**: Tonborg analyzes and records suggestions.
|
|
10
|
+
- **Shared decisions**: Tonborg applies routine knowledge reviews backed by existing confirmation and accepts verified FULI tasks.
|
|
11
|
+
- **Autonomous**: Tonborg may also review ordinary pending knowledge. Conflicts, sensitive preferences, uncertain evidence, and actions outside existing authority remain for the user.
|
|
12
|
+
|
|
13
|
+
Projects inherit the global policy unless the user sets an exception. Updating FULI does not silently expand existing authority. The selected local client runs the judgment, and quality preference controls its reasoning effort. Logs show the actual model only when the client reports it.
|
|
14
|
+
|
|
15
|
+
## What a decision does
|
|
16
|
+
|
|
17
|
+
Knowledge review uses bounded batches and an independent review ledger. A successful assessment appears as **Tonborg reviewed**. It does not change human confirmation, authoritative-source confirmation, publication eligibility, or the original evidence. The Provider compares an evidence snapshot within the write transaction; edited or conflicted knowledge loses its effective AI-reviewed disposition. A failing item cannot block the rest of the queue.
|
|
18
|
+
|
|
19
|
+
`assess_agent_action` recommends continuing the current conversation, reusing an authorized conversation, opening a session, or delegating work. It selects only registered, authorized, preflighted executors and available models that satisfy the Agent, assignment, task, and user routing constraints. The host performs and reports the actual launch or resumption. Conversation history is supplied only to a judgment client permitted for that Agent.
|
|
20
|
+
|
|
21
|
+
`assess_task_completion` is called by the accountable lead with its current task context, exact task revision, and artifact revision. Automatic acceptance requires a passed real verification by another Agent. The Provider rechecks the current lead context, task revision, artifact, and verified attempt before completing the FULI task. Jefa's separate human acceptance, publication, permissions, and payments retain their existing boundaries.
|
|
22
|
+
|
|
23
|
+
The console processes bounded automatic review batches while running. Manual review is available in Tonborg's workbench. Other MCP processes share the same local policy and decision database without starting duplicate schedulers. A lease prevents simultaneous decisions for the same project.
|
|
24
|
+
|
|
25
|
+
## Correct a judgment
|
|
26
|
+
|
|
27
|
+
Use thumbs up or thumbs down with an optional reason. A vote takes one click; reasons can be changed or cleared. The original decision, execution receipts, and feedback history remain auditable. Relevant feedback includes the selected client/model and work kind, so a reasonless vote still identifies the choice being evaluated. Feedback is scoped evidence for later decisions, not a new universal instruction or automatic reversal of an action.
|
|
28
|
+
|
|
29
|
+
Tonborg uses only its own scoped memory and authorized evidence. Client failure, malformed output, stale evidence, or missing authority produces a visible failure or escalation rather than a successful execution claim. Its judgment subprocess has no task tools or MCP access.
|
|
30
|
+
|
|
31
|
+
## Interfaces
|
|
32
|
+
|
|
33
|
+
The MCP tools are `get_judgment_policy`, `list_judgments`, `review_with_tonborg`, `assess_agent_action`, `assess_task_completion`, `get_agent_pins`, and `pin_agent`. Autonomy changes and feedback remain local user operations. Agent entry context supplies the current policy and workflow guidance; existing sessions should reconnect to discover newly installed tools.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Version-bound AI review evidence helpers.
|
|
2
|
+
|
|
3
|
+
An AI decision is useful only for the exact item snapshot that was supplied to
|
|
4
|
+
the model. Keep the digest deliberately independent from timestamps: edits can
|
|
5
|
+
be visible in the graph before a separate revision or audit timestamp moves.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
from datetime import date, datetime
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_DIRECT_FIELDS = (
|
|
17
|
+
'item_kind',
|
|
18
|
+
'item_id',
|
|
19
|
+
'title',
|
|
20
|
+
'content',
|
|
21
|
+
'valid_at',
|
|
22
|
+
'invalid_at',
|
|
23
|
+
'confirmation_status',
|
|
24
|
+
'confirmation_basis_json',
|
|
25
|
+
'profile_aspect',
|
|
26
|
+
'preference_scope',
|
|
27
|
+
'preference_project_id',
|
|
28
|
+
'preference_agent_id',
|
|
29
|
+
'inheritance_mode',
|
|
30
|
+
'current_quadrant',
|
|
31
|
+
'utility_score',
|
|
32
|
+
'confidence_score',
|
|
33
|
+
'qualified_use_count',
|
|
34
|
+
'distinct_task_count',
|
|
35
|
+
'negative_evidence_count',
|
|
36
|
+
'requires_attention',
|
|
37
|
+
'human_change_version',
|
|
38
|
+
'attributes_json',
|
|
39
|
+
'attributes',
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _json_safe(value: Any) -> Any:
|
|
44
|
+
if isinstance(value, (datetime, date)):
|
|
45
|
+
return value.isoformat()
|
|
46
|
+
to_native = getattr(value, 'to_native', None)
|
|
47
|
+
if callable(to_native):
|
|
48
|
+
return _json_safe(to_native())
|
|
49
|
+
if hasattr(value, 'model_dump'):
|
|
50
|
+
return _json_safe(value.model_dump(mode='json'))
|
|
51
|
+
if isinstance(value, dict):
|
|
52
|
+
return {str(key): _json_safe(item) for key, item in value.items()}
|
|
53
|
+
if isinstance(value, set):
|
|
54
|
+
return sorted((_json_safe(item) for item in value), key=repr)
|
|
55
|
+
if isinstance(value, (list, tuple)):
|
|
56
|
+
return [_json_safe(item) for item in value]
|
|
57
|
+
return value
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _canonical_value(item: dict[str, Any], key: str) -> Any:
|
|
61
|
+
value = item.get(key)
|
|
62
|
+
if key == 'attributes' and value is None:
|
|
63
|
+
value = item.get('attributes_json')
|
|
64
|
+
if key == 'attributes_json' and value is None:
|
|
65
|
+
value = item.get('attributes')
|
|
66
|
+
if key in {'attributes', 'attributes_json'}:
|
|
67
|
+
if value in (None, '', '{}'):
|
|
68
|
+
return {}
|
|
69
|
+
if isinstance(value, str):
|
|
70
|
+
try:
|
|
71
|
+
value = json.loads(value)
|
|
72
|
+
except (TypeError, ValueError):
|
|
73
|
+
pass
|
|
74
|
+
return _json_safe(value)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def ai_review_evidence_payload(
|
|
78
|
+
item: dict[str, Any],
|
|
79
|
+
*,
|
|
80
|
+
scope: str,
|
|
81
|
+
personal_project_id: str | None,
|
|
82
|
+
has_conflict: bool,
|
|
83
|
+
) -> dict[str, Any]:
|
|
84
|
+
"""Build the complete evidence identity used by candidate and CAS paths."""
|
|
85
|
+
|
|
86
|
+
payload = {
|
|
87
|
+
'direct': {
|
|
88
|
+
key: _canonical_value(item, key)
|
|
89
|
+
for key in _DIRECT_FIELDS
|
|
90
|
+
},
|
|
91
|
+
'scope': scope,
|
|
92
|
+
'personal_project_id': personal_project_id,
|
|
93
|
+
'project_ids': sorted({
|
|
94
|
+
str(project_id)
|
|
95
|
+
for project_id in (item.get('project_ids') or [])
|
|
96
|
+
if project_id
|
|
97
|
+
}),
|
|
98
|
+
'has_conflict': bool(has_conflict),
|
|
99
|
+
}
|
|
100
|
+
return _json_safe(payload)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def ai_review_evidence_token(
|
|
104
|
+
item: dict[str, Any],
|
|
105
|
+
*,
|
|
106
|
+
scope: str,
|
|
107
|
+
personal_project_id: str | None,
|
|
108
|
+
has_conflict: bool,
|
|
109
|
+
) -> str:
|
|
110
|
+
"""Return a deterministic SHA-256 token for the reviewable item snapshot."""
|
|
111
|
+
|
|
112
|
+
encoded = json.dumps(
|
|
113
|
+
ai_review_evidence_payload(
|
|
114
|
+
item,
|
|
115
|
+
scope=scope,
|
|
116
|
+
personal_project_id=personal_project_id,
|
|
117
|
+
has_conflict=has_conflict,
|
|
118
|
+
),
|
|
119
|
+
ensure_ascii=False,
|
|
120
|
+
sort_keys=True,
|
|
121
|
+
separators=(',', ':'),
|
|
122
|
+
).encode('utf-8')
|
|
123
|
+
return hashlib.sha256(encoded).hexdigest()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def effective_ai_review(
|
|
127
|
+
item: dict[str, Any],
|
|
128
|
+
*,
|
|
129
|
+
stored_token: str | None,
|
|
130
|
+
stored_assessment: dict[str, Any] | str | None,
|
|
131
|
+
scope: str,
|
|
132
|
+
personal_project_id: str | None,
|
|
133
|
+
has_conflict: bool,
|
|
134
|
+
) -> dict[str, Any] | None:
|
|
135
|
+
"""Expose an assessment only while its exact current evidence still holds."""
|
|
136
|
+
|
|
137
|
+
if not stored_token or item.get('requires_attention') is True or has_conflict:
|
|
138
|
+
return None
|
|
139
|
+
current_token = ai_review_evidence_token(
|
|
140
|
+
item,
|
|
141
|
+
scope=scope,
|
|
142
|
+
personal_project_id=personal_project_id,
|
|
143
|
+
has_conflict=has_conflict,
|
|
144
|
+
)
|
|
145
|
+
if current_token != stored_token:
|
|
146
|
+
return None
|
|
147
|
+
if isinstance(stored_assessment, str):
|
|
148
|
+
try:
|
|
149
|
+
stored_assessment = json.loads(stored_assessment)
|
|
150
|
+
except (TypeError, ValueError):
|
|
151
|
+
return None
|
|
152
|
+
if not isinstance(stored_assessment, dict):
|
|
153
|
+
return None
|
|
154
|
+
return stored_assessment
|
|
@@ -330,6 +330,18 @@ def _promotion_query(item_kind):
|
|
|
330
330
|
WITH space, parent, canonical, direct_children,
|
|
331
331
|
collect({{item: duplicate, spec: duplicate_spec}}) AS duplicate_pairs
|
|
332
332
|
WHERE size(duplicate_pairs) = size($duplicate_specs)
|
|
333
|
+
WITH space, parent, canonical, direct_children, duplicate_pairs,
|
|
334
|
+
[{{kind: $item_kind, item: canonical}}] +
|
|
335
|
+
[pair IN duplicate_pairs |
|
|
336
|
+
{{kind: $item_kind, item: pair.item}}] AS lock_items
|
|
337
|
+
UNWIND lock_items AS lock_item
|
|
338
|
+
WITH space, parent, canonical, direct_children,
|
|
339
|
+
duplicate_pairs, lock_item
|
|
340
|
+
ORDER BY lock_item.kind + ':' + lock_item.item.uuid
|
|
341
|
+
SET lock_item.item.fuli_ai_review_lock_version =
|
|
342
|
+
coalesce(lock_item.item.fuli_ai_review_lock_version, 0) + 1
|
|
343
|
+
WITH space, parent, canonical, direct_children,
|
|
344
|
+
duplicate_pairs, collect(lock_item) AS _locked_items
|
|
333
345
|
SET canonical.fuli_inheritance_mode = 'descendants',
|
|
334
346
|
canonical.fuli_inherited_project_ids = []
|
|
335
347
|
{duplicate_update}
|
|
@@ -55,6 +55,13 @@ class GraphNode(StrictModel):
|
|
|
55
55
|
last_human_changed_at: datetime | None = None
|
|
56
56
|
last_agent_viewed_at: datetime | None = None
|
|
57
57
|
last_agent_reviewed_at: datetime | None = None
|
|
58
|
+
ai_review_evidence_token: str | None = None
|
|
59
|
+
ai_assessment: dict[str, Any] | None = None
|
|
60
|
+
ai_review_scope: str | None = None
|
|
61
|
+
ai_review_project_id: str | None = None
|
|
62
|
+
ai_reviewed_at: datetime | None = None
|
|
63
|
+
ai_review_id: str | None = None
|
|
64
|
+
ai_review_decision_id: str | None = None
|
|
58
65
|
utility_score: float = Field(default=0, ge=0, le=1)
|
|
59
66
|
confidence_score: float = Field(default=0.5, ge=0, le=1)
|
|
60
67
|
qualified_use_count: int = Field(default=0, ge=0)
|
|
@@ -109,6 +116,13 @@ class GraphEdge(StrictModel):
|
|
|
109
116
|
last_human_changed_at: datetime | None = None
|
|
110
117
|
last_agent_viewed_at: datetime | None = None
|
|
111
118
|
last_agent_reviewed_at: datetime | None = None
|
|
119
|
+
ai_review_evidence_token: str | None = None
|
|
120
|
+
ai_assessment: dict[str, Any] | None = None
|
|
121
|
+
ai_review_scope: str | None = None
|
|
122
|
+
ai_review_project_id: str | None = None
|
|
123
|
+
ai_reviewed_at: datetime | None = None
|
|
124
|
+
ai_review_id: str | None = None
|
|
125
|
+
ai_review_decision_id: str | None = None
|
|
112
126
|
utility_score: float = Field(default=0, ge=0, le=1)
|
|
113
127
|
confidence_score: float = Field(default=0.5, ge=0, le=1)
|
|
114
128
|
qualified_use_count: int = Field(default=0, ge=0)
|
|
@@ -4,6 +4,7 @@ from .graph_projection import (
|
|
|
4
4
|
management_projection,
|
|
5
5
|
personal_project_projection,
|
|
6
6
|
)
|
|
7
|
+
from .ai_review import effective_ai_review
|
|
7
8
|
from .graph_models import GraphEdge, GraphNode, GraphResult
|
|
8
9
|
from .models import (
|
|
9
10
|
ConfirmationBasis,
|
|
@@ -82,6 +83,7 @@ async def read_graph(
|
|
|
82
83
|
assignments = await _read_assignments(store, space_id, item_ids)
|
|
83
84
|
references = await read_project_references(store, space_id, item_ids)
|
|
84
85
|
conflicts = await read_knowledge_conflicts(store, space_id, item_ids)
|
|
86
|
+
preference_conflicts = await _read_preference_conflicts(store, space_id, item_ids)
|
|
85
87
|
audits = await read_knowledge_audits(store, space_id, item_ids)
|
|
86
88
|
knowledge_nodes = [
|
|
87
89
|
_graph_node(
|
|
@@ -92,6 +94,9 @@ async def read_graph(
|
|
|
92
94
|
references,
|
|
93
95
|
conflicts,
|
|
94
96
|
audits,
|
|
97
|
+
preference_conflicts=preference_conflicts,
|
|
98
|
+
review_scope='project' if project else 'all',
|
|
99
|
+
personal_project_id=personal_project_id,
|
|
95
100
|
)
|
|
96
101
|
for record in node_records
|
|
97
102
|
]
|
|
@@ -104,6 +109,9 @@ async def read_graph(
|
|
|
104
109
|
references,
|
|
105
110
|
conflicts,
|
|
106
111
|
audits,
|
|
112
|
+
preference_conflicts=preference_conflicts,
|
|
113
|
+
review_scope='project' if project else 'all',
|
|
114
|
+
personal_project_id=personal_project_id,
|
|
107
115
|
)
|
|
108
116
|
for record in edge_records
|
|
109
117
|
]
|
|
@@ -229,6 +237,13 @@ def _node_query(project_scoped: bool, paginated: bool = False) -> str:
|
|
|
229
237
|
node.fuli_last_human_changed_at AS last_human_changed_at,
|
|
230
238
|
node.fuli_last_agent_viewed_at AS last_agent_viewed_at,
|
|
231
239
|
node.fuli_last_agent_reviewed_at AS last_agent_reviewed_at,
|
|
240
|
+
node.fuli_ai_review_evidence_token AS ai_review_evidence_token,
|
|
241
|
+
node.fuli_ai_assessment_json AS ai_assessment_json,
|
|
242
|
+
node.fuli_ai_review_scope AS ai_review_scope,
|
|
243
|
+
node.fuli_ai_review_project_id AS ai_review_project_id,
|
|
244
|
+
node.fuli_ai_reviewed_at AS ai_reviewed_at,
|
|
245
|
+
node.fuli_ai_review_id AS ai_review_id,
|
|
246
|
+
node.fuli_ai_review_decision_id AS ai_review_decision_id,
|
|
232
247
|
coalesce(node.fuli_utility_score, 0.0) AS utility_score,
|
|
233
248
|
coalesce(node.fuli_confidence_score, 0.5) AS confidence_score,
|
|
234
249
|
coalesce(node.fuli_qualified_use_count, 0)
|
|
@@ -345,6 +360,13 @@ def _edge_query(project_scoped: bool, paginated: bool = False) -> str:
|
|
|
345
360
|
edge.fuli_last_human_changed_at AS last_human_changed_at,
|
|
346
361
|
edge.fuli_last_agent_viewed_at AS last_agent_viewed_at,
|
|
347
362
|
edge.fuli_last_agent_reviewed_at AS last_agent_reviewed_at,
|
|
363
|
+
edge.fuli_ai_review_evidence_token AS ai_review_evidence_token,
|
|
364
|
+
edge.fuli_ai_assessment_json AS ai_assessment_json,
|
|
365
|
+
edge.fuli_ai_review_scope AS ai_review_scope,
|
|
366
|
+
edge.fuli_ai_review_project_id AS ai_review_project_id,
|
|
367
|
+
edge.fuli_ai_reviewed_at AS ai_reviewed_at,
|
|
368
|
+
edge.fuli_ai_review_id AS ai_review_id,
|
|
369
|
+
edge.fuli_ai_review_decision_id AS ai_review_decision_id,
|
|
348
370
|
coalesce(edge.fuli_utility_score, 0.0) AS utility_score,
|
|
349
371
|
coalesce(edge.fuli_confidence_score, 0.5) AS confidence_score,
|
|
350
372
|
coalesce(edge.fuli_qualified_use_count, 0)
|
|
@@ -462,6 +484,31 @@ async def _read_assignments(store, space_id: str, item_ids: list[str]):
|
|
|
462
484
|
return result
|
|
463
485
|
|
|
464
486
|
|
|
487
|
+
async def _read_preference_conflicts(store, space_id: str, item_ids: list[str]):
|
|
488
|
+
if not item_ids:
|
|
489
|
+
return {}
|
|
490
|
+
records, _, _ = await store.runtime.driver.execute_query(
|
|
491
|
+
'''
|
|
492
|
+
MATCH (:FuliSpace {id: $space_id, kind: 'personal'})-
|
|
493
|
+
[:HAS_PREFERENCE_CONFLICT]->
|
|
494
|
+
(conflict:FuliPreferenceConflict {status: 'ai_pending'})
|
|
495
|
+
WHERE conflict.left_item_id IN $item_ids
|
|
496
|
+
OR conflict.right_item_id IN $item_ids
|
|
497
|
+
RETURN conflict
|
|
498
|
+
''',
|
|
499
|
+
space_id=space_id,
|
|
500
|
+
item_ids=item_ids,
|
|
501
|
+
routing_='r',
|
|
502
|
+
)
|
|
503
|
+
result = {}
|
|
504
|
+
for record in records:
|
|
505
|
+
value = dict(record['conflict'])
|
|
506
|
+
for item_id in (value.get('left_item_id'), value.get('right_item_id')):
|
|
507
|
+
if item_id in item_ids:
|
|
508
|
+
result.setdefault(item_id, []).append(value)
|
|
509
|
+
return result
|
|
510
|
+
|
|
511
|
+
|
|
465
512
|
def _graph_node(
|
|
466
513
|
record,
|
|
467
514
|
evidence,
|
|
@@ -470,12 +517,27 @@ def _graph_node(
|
|
|
470
517
|
references,
|
|
471
518
|
conflicts,
|
|
472
519
|
audits=None,
|
|
520
|
+
*,
|
|
521
|
+
preference_conflicts=None,
|
|
522
|
+
review_scope: str = 'all',
|
|
523
|
+
personal_project_id: str | None = None,
|
|
473
524
|
) -> GraphNode:
|
|
474
525
|
episode_ids = record.get('episodes') or []
|
|
475
526
|
status, basis, confidence, confirmation_explicit = _confirmation_projection(
|
|
476
527
|
record
|
|
477
528
|
)
|
|
478
529
|
attributes = json_object(record.get('attributes_json'))
|
|
530
|
+
ai_assessment = _effective_ai_projection(
|
|
531
|
+
record,
|
|
532
|
+
item_kind='entity',
|
|
533
|
+
evidence=evidence,
|
|
534
|
+
assignments=assignments,
|
|
535
|
+
references=references,
|
|
536
|
+
conflicts=conflicts,
|
|
537
|
+
preference_conflicts=preference_conflicts or {},
|
|
538
|
+
review_scope=review_scope,
|
|
539
|
+
personal_project_id=personal_project_id,
|
|
540
|
+
)
|
|
479
541
|
return GraphNode(
|
|
480
542
|
id=record['id'],
|
|
481
543
|
name=record['name'],
|
|
@@ -516,6 +578,14 @@ def _graph_node(
|
|
|
516
578
|
last_agent_reviewed_at=_native_datetime(
|
|
517
579
|
record.get('last_agent_reviewed_at')
|
|
518
580
|
),
|
|
581
|
+
ai_review_evidence_token=record.get('ai_review_evidence_token'),
|
|
582
|
+
ai_assessment=ai_assessment,
|
|
583
|
+
ai_review_scope=record.get('ai_review_scope'),
|
|
584
|
+
ai_review_project_id=record.get('ai_review_project_id'),
|
|
585
|
+
ai_reviewed_at=_native_datetime(record.get('ai_reviewed_at'))
|
|
586
|
+
if ai_assessment is not None else None,
|
|
587
|
+
ai_review_id=record.get('ai_review_id'),
|
|
588
|
+
ai_review_decision_id=record.get('ai_review_decision_id'),
|
|
519
589
|
utility_score=float(record.get('utility_score') or 0),
|
|
520
590
|
confidence_score=confidence,
|
|
521
591
|
qualified_use_count=int(record.get('qualified_use_count') or 0),
|
|
@@ -550,12 +620,27 @@ def _graph_edge(
|
|
|
550
620
|
references,
|
|
551
621
|
conflicts,
|
|
552
622
|
audits=None,
|
|
623
|
+
*,
|
|
624
|
+
preference_conflicts=None,
|
|
625
|
+
review_scope: str = 'all',
|
|
626
|
+
personal_project_id: str | None = None,
|
|
553
627
|
) -> GraphEdge:
|
|
554
628
|
episode_ids = record.get('episodes') or []
|
|
555
629
|
status, basis, confidence, confirmation_explicit = _confirmation_projection(
|
|
556
630
|
record
|
|
557
631
|
)
|
|
558
632
|
attributes = json_object(record.get('attributes_json'))
|
|
633
|
+
ai_assessment = _effective_ai_projection(
|
|
634
|
+
record,
|
|
635
|
+
item_kind='relationship',
|
|
636
|
+
evidence=evidence,
|
|
637
|
+
assignments=assignments,
|
|
638
|
+
references=references,
|
|
639
|
+
conflicts=conflicts,
|
|
640
|
+
preference_conflicts=preference_conflicts or {},
|
|
641
|
+
review_scope=review_scope,
|
|
642
|
+
personal_project_id=personal_project_id,
|
|
643
|
+
)
|
|
559
644
|
return GraphEdge(
|
|
560
645
|
id=record['id'],
|
|
561
646
|
source=record['source'],
|
|
@@ -598,6 +683,14 @@ def _graph_edge(
|
|
|
598
683
|
last_agent_reviewed_at=_native_datetime(
|
|
599
684
|
record.get('last_agent_reviewed_at')
|
|
600
685
|
),
|
|
686
|
+
ai_review_evidence_token=record.get('ai_review_evidence_token'),
|
|
687
|
+
ai_assessment=ai_assessment,
|
|
688
|
+
ai_review_scope=record.get('ai_review_scope'),
|
|
689
|
+
ai_review_project_id=record.get('ai_review_project_id'),
|
|
690
|
+
ai_reviewed_at=_native_datetime(record.get('ai_reviewed_at'))
|
|
691
|
+
if ai_assessment is not None else None,
|
|
692
|
+
ai_review_id=record.get('ai_review_id'),
|
|
693
|
+
ai_review_decision_id=record.get('ai_review_decision_id'),
|
|
601
694
|
utility_score=float(record.get('utility_score') or 0),
|
|
602
695
|
confidence_score=confidence,
|
|
603
696
|
qualified_use_count=int(record.get('qualified_use_count') or 0),
|
|
@@ -658,3 +751,127 @@ def _confirmation_projection(record) -> tuple[
|
|
|
658
751
|
and basis is not None
|
|
659
752
|
)
|
|
660
753
|
return status, basis, confidence, explicit
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def _field_value(value, name: str, default=None):
|
|
757
|
+
if isinstance(value, dict):
|
|
758
|
+
return value.get(name, default)
|
|
759
|
+
return getattr(value, name, default)
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
def _effective_ai_projection(
|
|
763
|
+
record,
|
|
764
|
+
*,
|
|
765
|
+
item_kind: str,
|
|
766
|
+
evidence,
|
|
767
|
+
assignments,
|
|
768
|
+
references,
|
|
769
|
+
conflicts,
|
|
770
|
+
preference_conflicts,
|
|
771
|
+
review_scope: str,
|
|
772
|
+
personal_project_id: str | None,
|
|
773
|
+
) -> dict | None:
|
|
774
|
+
# The display scope may be broader than the scope used to produce the
|
|
775
|
+
# assessment. Validate against the persisted review scope so a
|
|
776
|
+
# project-bound token remains effective in an authorized all-space view.
|
|
777
|
+
stored_review_scope = record.get('ai_review_scope')
|
|
778
|
+
stored_review_project_id = record.get('ai_review_project_id')
|
|
779
|
+
validation_scope = stored_review_scope or review_scope
|
|
780
|
+
validation_project_id = (
|
|
781
|
+
stored_review_project_id
|
|
782
|
+
if stored_review_project_id is not None
|
|
783
|
+
else personal_project_id
|
|
784
|
+
)
|
|
785
|
+
if (
|
|
786
|
+
stored_review_project_id is not None
|
|
787
|
+
and review_scope in {'project', 'preferences_project'}
|
|
788
|
+
and personal_project_id != stored_review_project_id
|
|
789
|
+
):
|
|
790
|
+
return None
|
|
791
|
+
project_scoped_review = validation_scope in {'project', 'preferences_project'}
|
|
792
|
+
item_id = record['id']
|
|
793
|
+
project_ids = {
|
|
794
|
+
str(project_id)
|
|
795
|
+
for project_id in (record.get('project_ids') or [])
|
|
796
|
+
if project_id
|
|
797
|
+
}
|
|
798
|
+
preference_project_id = record.get('preference_project_id')
|
|
799
|
+
if preference_project_id:
|
|
800
|
+
project_ids.add(str(preference_project_id))
|
|
801
|
+
for assignment in assignments.get(item_id, []):
|
|
802
|
+
project_id = _field_value(assignment, 'project_id')
|
|
803
|
+
if project_id:
|
|
804
|
+
project_ids.add(str(project_id))
|
|
805
|
+
for reference in references.get(item_id, []):
|
|
806
|
+
project_id = _field_value(reference, 'project_id')
|
|
807
|
+
if project_id and _field_value(reference, 'status') == 'active':
|
|
808
|
+
project_ids.add(str(project_id))
|
|
809
|
+
for episode_id in record.get('episodes') or []:
|
|
810
|
+
project_id = _field_value(evidence.get(episode_id), 'personal_project_id')
|
|
811
|
+
if project_id:
|
|
812
|
+
project_ids.add(str(project_id))
|
|
813
|
+
|
|
814
|
+
scoped_conflict = False
|
|
815
|
+
for conflict in conflicts.get(item_id, []):
|
|
816
|
+
if _field_value(conflict, 'status') != 'pending':
|
|
817
|
+
continue
|
|
818
|
+
if not project_scoped_review or validation_project_id is None:
|
|
819
|
+
scoped_conflict = True
|
|
820
|
+
break
|
|
821
|
+
target_project_id = _field_value(conflict, 'target_project_id')
|
|
822
|
+
source_project_id = _field_value(conflict, 'source_project_id')
|
|
823
|
+
if validation_project_id in {target_project_id, source_project_id}:
|
|
824
|
+
scoped_conflict = True
|
|
825
|
+
break
|
|
826
|
+
if not scoped_conflict:
|
|
827
|
+
for conflict in preference_conflicts.get(item_id, []):
|
|
828
|
+
if _field_value(conflict, 'status') != 'ai_pending':
|
|
829
|
+
continue
|
|
830
|
+
if not project_scoped_review or validation_project_id is None:
|
|
831
|
+
scoped_conflict = True
|
|
832
|
+
break
|
|
833
|
+
if _field_value(conflict, 'preference_project_id') == validation_project_id:
|
|
834
|
+
scoped_conflict = True
|
|
835
|
+
break
|
|
836
|
+
|
|
837
|
+
item = {
|
|
838
|
+
'item_kind': item_kind,
|
|
839
|
+
'item_id': item_id,
|
|
840
|
+
'title': (
|
|
841
|
+
record.get('name') or item_id
|
|
842
|
+
if item_kind == 'entity'
|
|
843
|
+
else record.get('type') or 'RELATES_TO'
|
|
844
|
+
),
|
|
845
|
+
'content': (
|
|
846
|
+
record.get('summary') or ''
|
|
847
|
+
if item_kind == 'entity'
|
|
848
|
+
else record.get('fact') or ''
|
|
849
|
+
),
|
|
850
|
+
'valid_at': record.get('valid_at'),
|
|
851
|
+
'invalid_at': record.get('invalid_at'),
|
|
852
|
+
'confirmation_status': record.get('confirmation_status'),
|
|
853
|
+
'confirmation_basis_json': record.get('confirmation_basis_json'),
|
|
854
|
+
'profile_aspect': record.get('profile_aspect'),
|
|
855
|
+
'preference_scope': record.get('preference_scope'),
|
|
856
|
+
'preference_project_id': preference_project_id,
|
|
857
|
+
'preference_agent_id': record.get('preference_agent_id'),
|
|
858
|
+
'inheritance_mode': record.get('inheritance_mode') or 'local_only',
|
|
859
|
+
'current_quadrant': record.get('current_quadrant') or 'known_known',
|
|
860
|
+
'utility_score': record.get('utility_score'),
|
|
861
|
+
'confidence_score': record.get('confidence_score'),
|
|
862
|
+
'qualified_use_count': record.get('qualified_use_count'),
|
|
863
|
+
'distinct_task_count': record.get('distinct_task_count'),
|
|
864
|
+
'negative_evidence_count': record.get('negative_evidence_count'),
|
|
865
|
+
'requires_attention': record.get('requires_attention') is True,
|
|
866
|
+
'human_change_version': record.get('human_change_version') or 0,
|
|
867
|
+
'attributes_json': record.get('attributes_json'),
|
|
868
|
+
'project_ids': sorted(project_ids),
|
|
869
|
+
}
|
|
870
|
+
return effective_ai_review(
|
|
871
|
+
item,
|
|
872
|
+
stored_token=record.get('ai_review_evidence_token'),
|
|
873
|
+
stored_assessment=record.get('ai_assessment_json'),
|
|
874
|
+
scope=validation_scope,
|
|
875
|
+
personal_project_id=validation_project_id,
|
|
876
|
+
has_conflict=scoped_conflict,
|
|
877
|
+
)
|
|
@@ -630,6 +630,8 @@ def _human_change_query(item_kind: str) -> str:
|
|
|
630
630
|
return f'''
|
|
631
631
|
MATCH (space:FuliSpace {{id: $space_id, kind: 'personal'}})
|
|
632
632
|
{_item_match(item_kind)}
|
|
633
|
+
SET item.fuli_ai_review_lock_version =
|
|
634
|
+
coalesce(item.fuli_ai_review_lock_version, 0) + 1
|
|
633
635
|
SET item.fuli_human_edited = true,
|
|
634
636
|
item.fuli_human_change_version =
|
|
635
637
|
coalesce(item.fuli_human_change_version, 0) + 1,
|
|
@@ -797,6 +799,8 @@ def _knowledge_feedback_query(item_kind: str) -> str:
|
|
|
797
799
|
MATCH (space:FuliSpace {{id: $space_id, kind: 'personal'}})
|
|
798
800
|
{_item_match(item_kind)}
|
|
799
801
|
WHERE coalesce(item.fuli_usage_generation, 1) = $usage_generation
|
|
802
|
+
SET item.fuli_ai_review_lock_version =
|
|
803
|
+
coalesce(item.fuli_ai_review_lock_version, 0) + 1
|
|
800
804
|
MERGE (audit:FuliKnowledgeAudit {{id: $audit_id}})
|
|
801
805
|
ON CREATE SET audit.space_id = $space_id,
|
|
802
806
|
audit.item_id = $item_id,
|
|
@@ -850,6 +854,8 @@ def _usage_score_update_query(item_kind: str) -> str:
|
|
|
850
854
|
return f'''
|
|
851
855
|
{_item_match(item_kind)}
|
|
852
856
|
WHERE coalesce(item.fuli_usage_generation, 1) = $usage_generation
|
|
857
|
+
SET item.fuli_ai_review_lock_version =
|
|
858
|
+
coalesce(item.fuli_ai_review_lock_version, 0) + 1
|
|
853
859
|
SET item.fuli_utility_score = $utility_score,
|
|
854
860
|
item.fuli_confidence_score = $confidence_score,
|
|
855
861
|
item.fuli_qualified_use_count = $qualified_use_count,
|