fuli-context 0.2.0 → 0.4.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/README.md +94 -25
- package/dist/web/assets/ConnectionsPage-CCKWm-6p.js +1 -0
- package/dist/web/assets/KnowledgeInspector-BZkB2wJn.js +1 -0
- package/dist/web/assets/KnowledgeInspector-CrGYxEob.css +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-CJCNI2Wd.css +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-PXH-TlGt.js +1 -0
- package/dist/web/assets/KnowledgePage-CNQXd15y.js +1 -0
- package/dist/web/assets/KnowledgeWorkspace-CXPE4GCD.js +4 -0
- package/dist/web/assets/OverviewPage-ul7g7HXh.js +1 -0
- package/dist/web/assets/PersonalProfilePage-CC40Pw2L.js +1 -0
- package/dist/web/assets/PersonalProfilePage-S8ZcG2LU.css +1 -0
- package/dist/web/assets/PersonalProjectsPage-CPIEcShD.js +1 -0
- package/dist/web/assets/PublicProjectsPage-C700sR7z.js +1 -0
- package/dist/web/assets/ReviewPage-7MDDUDgA.js +1 -0
- package/dist/web/assets/console-B_Ena2DS.js +5 -0
- package/dist/web/assets/identity-CjBwYSMX.js +1 -0
- package/dist/web/assets/index-BQRkOwPx.js +2 -0
- package/dist/web/assets/{index-Devf7glZ.css → index-CpLDx3of.css} +1 -1
- package/dist/web/assets/model-C-5RVIJ2.js +1 -0
- package/dist/web/assets/{paths-B790iniO.js → paths-BkULqmGI.js} +1 -1
- package/dist/web/assets/{vue-router-BYTAoWT3.js → vue-router-DnD3hUm7.js} +1 -1
- package/dist/web/index.html +5 -6
- package/graph-provider/fuli_graph/app.py +48 -0
- package/graph-provider/fuli_graph/common_knowledge.py +443 -0
- package/graph-provider/fuli_graph/common_knowledge_models.py +54 -0
- package/graph-provider/fuli_graph/graph_models.py +9 -0
- package/graph-provider/fuli_graph/graph_query.py +25 -0
- package/graph-provider/fuli_graph/knowledge_audit.py +211 -0
- package/graph-provider/fuli_graph/knowledge_feedback_models.py +69 -0
- package/graph-provider/fuli_graph/knowledge_management.py +37 -0
- package/graph-provider/fuli_graph/knowledge_search.py +24 -2
- package/graph-provider/fuli_graph/models.py +69 -0
- package/graph-provider/fuli_graph/search_projection.py +59 -0
- package/graph-provider/fuli_graph/store_knowledge.py +2 -0
- package/graph-provider/pyproject.toml +1 -1
- package/npm-shrinkwrap.json +97 -2
- package/package.json +2 -1
- package/skills/capturing-session-knowledge/SKILL.md +86 -15
- package/src/agent-tools/graph-definitions.js +181 -13
- package/src/agent-tools/graph-handlers.js +19 -0
- package/src/cli/command-registry.js +6 -59
- package/src/cli/update-command.js +293 -0
- package/src/cli.js +7 -34
- package/src/graphiti/agent-knowledge-workflows.js +543 -0
- package/src/graphiti/federated-application.js +85 -104
- package/src/graphiti/knowledge-provider-mapping.js +91 -0
- package/src/graphiti/provider-client.js +15 -0
- package/src/graphiti/source-marker.js +1 -7
- package/src/graphiti/source-uri.js +29 -0
- package/src/http/static-handler.js +29 -3
- package/src/mcp/common-knowledge-preview-tokens.js +67 -0
- package/src/mcp/create-mcp-server.js +43 -7
- package/src/mcp/instructions.js +7 -8
- package/src/mcp/lifecycle-audit.js +27 -0
- package/src/mcp/task-context-registry.js +93 -0
- package/src/mcp/tool-annotations.js +16 -1
- package/src/mcp/tool-result.js +24 -0
- package/src/runtime-options.js +7 -8
- package/src/setup/agent-installation-status.js +14 -2
- package/src/setup/agents.js +12 -1
- package/src/setup/claude-code-config.js +194 -0
- package/src/setup/config-backup.js +20 -5
- package/src/setup/options.js +9 -1
- package/dist/web/assets/ConnectionsPage-CY6H_mJJ.js +0 -1
- package/dist/web/assets/KnowledgeInspector-BSpz5Sfm.js +0 -1
- package/dist/web/assets/KnowledgeInspector-CHbB_0rC.css +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-BlEvhPrv.css +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-CvjGPlUc.js +0 -1
- package/dist/web/assets/KnowledgePage-Khuo3cNs.js +0 -1
- package/dist/web/assets/KnowledgeWorkspace-DVZusvem.js +0 -4
- package/dist/web/assets/OverviewPage-DC4C4Fhn.js +0 -1
- package/dist/web/assets/PersonalProfilePage-CSV9Mtgz.js +0 -1
- package/dist/web/assets/PersonalProfilePage-CVJ3TcRi.css +0 -1
- package/dist/web/assets/PersonalProjectsPage-A50EaseQ.js +0 -1
- package/dist/web/assets/PublicProjectsPage-BAdO_bae.js +0 -1
- package/dist/web/assets/ReviewPage-Dztk2jDG.js +0 -1
- package/dist/web/assets/console-tbvTUi6p.js +0 -1
- package/dist/web/assets/identity-B3s5CXhv.js +0 -1
- package/dist/web/assets/index-DwUDhQKc.js +0 -6
- package/dist/web/assets/model-DVT8sjy9.js +0 -1
- package/dist/web/assets/runtime-dom.esm-bundler-DttsK21F.js +0 -1
- package/src/cli/commands/candidates.js +0 -17
- package/src/cli/commands/ingestion.js +0 -32
- package/src/cli/commands/reads.js +0 -50
- package/src/cli/commands/spaces.js +0 -20
- package/src/cli/invocation.js +0 -47
- package/src/cli/migrate-command.js +0 -71
- package/src/cli/migration-destination.js +0 -117
- package/src/storage/import-json.js +0 -70
- package/src/storage/legacy-record-validation.js +0 -152
- package/src/storage/legacy-snapshot-validation.js +0 -32
|
@@ -9,6 +9,11 @@ from .knowledge_usage_models import (
|
|
|
9
9
|
KnowledgeUsageItemResult,
|
|
10
10
|
KnowledgeUsageResult,
|
|
11
11
|
)
|
|
12
|
+
from .knowledge_feedback_models import (
|
|
13
|
+
KnowledgeFeedbackCreate,
|
|
14
|
+
KnowledgeFeedbackItemResult,
|
|
15
|
+
KnowledgeFeedbackResult,
|
|
16
|
+
)
|
|
12
17
|
from .models import (
|
|
13
18
|
KnowledgeAgentReviewCreate,
|
|
14
19
|
KnowledgeAgentViewCreate,
|
|
@@ -29,6 +34,12 @@ USAGE_WEIGHTS = {
|
|
|
29
34
|
'cited': (0.12, 0.04),
|
|
30
35
|
'applied': (0.18, 0.06),
|
|
31
36
|
}
|
|
37
|
+
NEGATIVE_EVIDENCE_WEIGHTS = {
|
|
38
|
+
'rejected': (0.35, 0.25),
|
|
39
|
+
'validation_failed': (0.35, 0.30),
|
|
40
|
+
'contradicted': (0.45, 0.40),
|
|
41
|
+
'outdated': (0.20, 0.20),
|
|
42
|
+
}
|
|
32
43
|
|
|
33
44
|
|
|
34
45
|
async def record_human_change(
|
|
@@ -147,6 +158,130 @@ async def record_knowledge_usage(
|
|
|
147
158
|
)
|
|
148
159
|
|
|
149
160
|
|
|
161
|
+
async def record_knowledge_feedback(
|
|
162
|
+
store,
|
|
163
|
+
actor: dict,
|
|
164
|
+
request: KnowledgeFeedbackCreate,
|
|
165
|
+
) -> KnowledgeFeedbackResult:
|
|
166
|
+
"""Record bounded negative evidence without overriding human authority."""
|
|
167
|
+
store._require_personal()
|
|
168
|
+
space = await store.authorize(actor, request.personal_space_id, 'maintainer')
|
|
169
|
+
if space['kind'] != 'personal':
|
|
170
|
+
raise HTTPException(status_code=422, detail='knowledge feedback is personal-only')
|
|
171
|
+
|
|
172
|
+
lock = store._group_locks.setdefault(space['group_id'], asyncio.Lock())
|
|
173
|
+
results = []
|
|
174
|
+
async with lock:
|
|
175
|
+
for item in request.items:
|
|
176
|
+
results.append(await _record_one_knowledge_feedback(
|
|
177
|
+
store, actor, space, request, item
|
|
178
|
+
))
|
|
179
|
+
return KnowledgeFeedbackResult(
|
|
180
|
+
recorded_count=sum(result.recorded for result in results),
|
|
181
|
+
duplicate_count=sum(not result.recorded for result in results),
|
|
182
|
+
items=results,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
async def _record_one_knowledge_feedback(store, actor, space, request, item):
|
|
187
|
+
state = await _read_usage_state(
|
|
188
|
+
store, space, item.item_id, item.item_kind
|
|
189
|
+
)
|
|
190
|
+
if state is None:
|
|
191
|
+
raise HTTPException(status_code=404, detail='knowledge item not found')
|
|
192
|
+
if state.get('invalid_at') is not None:
|
|
193
|
+
raise HTTPException(
|
|
194
|
+
status_code=409,
|
|
195
|
+
detail='historical knowledge cannot receive new feedback evidence',
|
|
196
|
+
)
|
|
197
|
+
generation = int(state.get('usage_generation') or 1)
|
|
198
|
+
previous_status = state.get('confirmation_status') or 'pending'
|
|
199
|
+
trusted_reporter = item.reported_by_kind in {
|
|
200
|
+
'user', 'authoritative_source'
|
|
201
|
+
}
|
|
202
|
+
next_status = (
|
|
203
|
+
'pending'
|
|
204
|
+
if previous_status == 'agent_confirmed' and trusted_reporter
|
|
205
|
+
else previous_status
|
|
206
|
+
)
|
|
207
|
+
basis = json_object(state.get('confirmation_basis_json'))
|
|
208
|
+
if next_status == 'pending' and previous_status == 'agent_confirmed':
|
|
209
|
+
basis = _pending_feedback_basis(basis, item.reason)
|
|
210
|
+
utility_weight, confidence_weight = NEGATIVE_EVIDENCE_WEIGHTS[
|
|
211
|
+
item.feedback_kind
|
|
212
|
+
]
|
|
213
|
+
utility_score = round(max(
|
|
214
|
+
0.0,
|
|
215
|
+
float(state.get('utility_score') or 0) - utility_weight,
|
|
216
|
+
), 4)
|
|
217
|
+
confidence_score = round(max(
|
|
218
|
+
0.0,
|
|
219
|
+
float(
|
|
220
|
+
state['confidence_score']
|
|
221
|
+
if state.get('confidence_score') is not None else 0.5
|
|
222
|
+
) - confidence_weight,
|
|
223
|
+
), 4)
|
|
224
|
+
feedback_at = datetime.now(timezone.utc)
|
|
225
|
+
audit_id = stable_uuid(
|
|
226
|
+
space['id'],
|
|
227
|
+
'knowledge-feedback',
|
|
228
|
+
item.item_kind,
|
|
229
|
+
item.item_id,
|
|
230
|
+
str(generation),
|
|
231
|
+
request.task_id,
|
|
232
|
+
item.feedback_kind,
|
|
233
|
+
)
|
|
234
|
+
records, _, _ = await store.runtime.driver.execute_query(
|
|
235
|
+
_knowledge_feedback_query(item.item_kind),
|
|
236
|
+
space_id=space['id'],
|
|
237
|
+
group_id=space['group_id'],
|
|
238
|
+
item_id=item.item_id,
|
|
239
|
+
item_kind=item.item_kind,
|
|
240
|
+
audit_id=audit_id,
|
|
241
|
+
task_id=request.task_id,
|
|
242
|
+
session_id=request.session_id,
|
|
243
|
+
tool_name=request.tool_name,
|
|
244
|
+
feedback_kind=item.feedback_kind,
|
|
245
|
+
reported_by_kind=item.reported_by_kind,
|
|
246
|
+
reason=item.reason,
|
|
247
|
+
evidence_summary=item.evidence_summary,
|
|
248
|
+
source_uri=item.source_uri,
|
|
249
|
+
usage_generation=generation,
|
|
250
|
+
next_confirmation_status=next_status,
|
|
251
|
+
next_confirmation_basis_json=(
|
|
252
|
+
json.dumps(basis, ensure_ascii=False, sort_keys=True)
|
|
253
|
+
if basis else None
|
|
254
|
+
),
|
|
255
|
+
utility_score=utility_score,
|
|
256
|
+
confidence_score=confidence_score,
|
|
257
|
+
created_by=actor['id'],
|
|
258
|
+
feedback_at=feedback_at,
|
|
259
|
+
)
|
|
260
|
+
if not records:
|
|
261
|
+
raise HTTPException(
|
|
262
|
+
status_code=409,
|
|
263
|
+
detail='knowledge changed while feedback evidence was recorded; retry',
|
|
264
|
+
)
|
|
265
|
+
record = dict(records[0])
|
|
266
|
+
return KnowledgeFeedbackItemResult(
|
|
267
|
+
item_id=item.item_id,
|
|
268
|
+
item_kind=item.item_kind,
|
|
269
|
+
feedback_kind=item.feedback_kind,
|
|
270
|
+
recorded=record.get('recorded') is True,
|
|
271
|
+
confirmation_status=record.get('confirmation_status') or next_status,
|
|
272
|
+
utility_score=float(record.get('utility_score') or 0),
|
|
273
|
+
confidence_score=float(
|
|
274
|
+
record['confidence_score']
|
|
275
|
+
if record.get('confidence_score') is not None else 0.5
|
|
276
|
+
),
|
|
277
|
+
negative_evidence_count=int(
|
|
278
|
+
record.get('negative_evidence_count') or 0
|
|
279
|
+
),
|
|
280
|
+
requires_attention=record.get('requires_attention') is True,
|
|
281
|
+
usage_generation=generation,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
150
285
|
async def _record_one_knowledge_use(store, actor, space, request, item):
|
|
151
286
|
state = await _read_usage_state(
|
|
152
287
|
store,
|
|
@@ -470,6 +605,10 @@ def _audit_record(value: dict) -> KnowledgeAuditRecord:
|
|
|
470
605
|
task_id=value.get('task_id'),
|
|
471
606
|
session_id=value.get('session_id'),
|
|
472
607
|
use_kind=value.get('use_kind'),
|
|
608
|
+
feedback_kind=value.get('feedback_kind'),
|
|
609
|
+
reported_by_kind=value.get('reported_by_kind'),
|
|
610
|
+
evidence_summary=value.get('evidence_summary'),
|
|
611
|
+
source_uri=value.get('source_uri'),
|
|
473
612
|
usage_generation=(
|
|
474
613
|
int(value['usage_generation'])
|
|
475
614
|
if value.get('usage_generation') is not None else None
|
|
@@ -592,6 +731,10 @@ async def _read_usage_state(store, space, item_id, item_kind):
|
|
|
592
731
|
AS confirmation_status,
|
|
593
732
|
item.fuli_confirmation_basis_json AS confirmation_basis_json,
|
|
594
733
|
coalesce(item.fuli_usage_generation, 1) AS usage_generation,
|
|
734
|
+
coalesce(item.fuli_utility_score, 0.0) AS utility_score,
|
|
735
|
+
coalesce(item.fuli_confidence_score, 0.5) AS confidence_score,
|
|
736
|
+
coalesce(item.fuli_negative_evidence_count, 0)
|
|
737
|
+
AS negative_evidence_count,
|
|
595
738
|
{
|
|
596
739
|
"item.fuli_invalid_at"
|
|
597
740
|
if item_kind == "entity" else "item.invalid_at"
|
|
@@ -649,6 +792,60 @@ def _knowledge_use_query(item_kind: str) -> str:
|
|
|
649
792
|
'''
|
|
650
793
|
|
|
651
794
|
|
|
795
|
+
def _knowledge_feedback_query(item_kind: str) -> str:
|
|
796
|
+
return f'''
|
|
797
|
+
MATCH (space:FuliSpace {{id: $space_id, kind: 'personal'}})
|
|
798
|
+
{_item_match(item_kind)}
|
|
799
|
+
WHERE coalesce(item.fuli_usage_generation, 1) = $usage_generation
|
|
800
|
+
MERGE (audit:FuliKnowledgeAudit {{id: $audit_id}})
|
|
801
|
+
ON CREATE SET audit.space_id = $space_id,
|
|
802
|
+
audit.item_id = $item_id,
|
|
803
|
+
audit.item_kind = $item_kind,
|
|
804
|
+
audit.action = 'knowledge_feedback',
|
|
805
|
+
audit.human_change_version = 0,
|
|
806
|
+
audit.task_id = $task_id,
|
|
807
|
+
audit.session_id = $session_id,
|
|
808
|
+
audit.tool_name = $tool_name,
|
|
809
|
+
audit.feedback_kind = $feedback_kind,
|
|
810
|
+
audit.reported_by_kind = $reported_by_kind,
|
|
811
|
+
audit.reason = $reason,
|
|
812
|
+
audit.evidence_summary = $evidence_summary,
|
|
813
|
+
audit.source_uri = $source_uri,
|
|
814
|
+
audit.usage_generation = $usage_generation,
|
|
815
|
+
audit.outcome = 'requires_attention',
|
|
816
|
+
audit.created_by = $created_by,
|
|
817
|
+
audit.created_at = $feedback_at
|
|
818
|
+
MERGE (space)-[:HAS_KNOWLEDGE_AUDIT]->(audit)
|
|
819
|
+
WITH item, audit, audit.created_at = $feedback_at AS recorded
|
|
820
|
+
FOREACH (_ IN CASE WHEN recorded THEN [1] ELSE [] END |
|
|
821
|
+
SET item.fuli_utility_score = $utility_score,
|
|
822
|
+
item.fuli_confidence_score = $confidence_score,
|
|
823
|
+
item.fuli_confirmation_status = $next_confirmation_status,
|
|
824
|
+
item.fuli_confirmation_basis_json =
|
|
825
|
+
$next_confirmation_basis_json,
|
|
826
|
+
item.fuli_epistemic_status =
|
|
827
|
+
CASE WHEN $next_confirmation_status = 'confirmed'
|
|
828
|
+
THEN 'confirmed'
|
|
829
|
+
WHEN item.fuli_origin_quadrant = 'unknown_unknown'
|
|
830
|
+
THEN 'exploratory'
|
|
831
|
+
ELSE 'observed' END,
|
|
832
|
+
item.fuli_negative_evidence_count =
|
|
833
|
+
coalesce(item.fuli_negative_evidence_count, 0) + 1,
|
|
834
|
+
item.fuli_requires_attention = true,
|
|
835
|
+
item.fuli_last_feedback_kind = $feedback_kind,
|
|
836
|
+
item.fuli_last_feedback_at = $feedback_at
|
|
837
|
+
)
|
|
838
|
+
RETURN recorded,
|
|
839
|
+
item.fuli_confirmation_status AS confirmation_status,
|
|
840
|
+
coalesce(item.fuli_utility_score, 0.0) AS utility_score,
|
|
841
|
+
coalesce(item.fuli_confidence_score, 0.5) AS confidence_score,
|
|
842
|
+
coalesce(item.fuli_negative_evidence_count, 0)
|
|
843
|
+
AS negative_evidence_count,
|
|
844
|
+
coalesce(item.fuli_requires_attention, false)
|
|
845
|
+
AS requires_attention
|
|
846
|
+
'''
|
|
847
|
+
|
|
848
|
+
|
|
652
849
|
def _usage_score_update_query(item_kind: str) -> str:
|
|
653
850
|
return f'''
|
|
654
851
|
{_item_match(item_kind)}
|
|
@@ -698,6 +895,20 @@ def _agent_confirmation_basis(value: dict, confirmed_at: datetime) -> dict:
|
|
|
698
895
|
}
|
|
699
896
|
|
|
700
897
|
|
|
898
|
+
def _pending_feedback_basis(value: dict, reason: str) -> dict:
|
|
899
|
+
result = dict(value or {})
|
|
900
|
+
result.setdefault('existence_reason', reason)
|
|
901
|
+
result.setdefault(
|
|
902
|
+
'quadrant_reason',
|
|
903
|
+
'The immutable discovery quadrant is unchanged by negative evidence.',
|
|
904
|
+
)
|
|
905
|
+
result.setdefault('proposed_by', {'kind': 'agent', 'label': 'Fuli'})
|
|
906
|
+
result['confirmed_by'] = None
|
|
907
|
+
result['confirmed_at'] = None
|
|
908
|
+
result.pop('agent_policy_version', None)
|
|
909
|
+
return result
|
|
910
|
+
|
|
911
|
+
|
|
701
912
|
_HUMAN_ENTITY_SEARCH = '''
|
|
702
913
|
MATCH (item:Entity {group_id: $group_id})
|
|
703
914
|
WHERE item.fuli_human_edited = true
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
3
|
+
from pydantic import Field, field_validator, model_validator
|
|
4
|
+
|
|
5
|
+
from .models import (
|
|
6
|
+
ConfirmationStatus,
|
|
7
|
+
KnowledgeFeedbackKind,
|
|
8
|
+
KnowledgeItemKind,
|
|
9
|
+
StrictModel,
|
|
10
|
+
_validated_source_uri,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
KnowledgeFeedbackReporterKind = Literal[
|
|
14
|
+
'user',
|
|
15
|
+
'agent',
|
|
16
|
+
'authoritative_source',
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class KnowledgeFeedbackItem(StrictModel):
|
|
21
|
+
item_id: str = Field(min_length=1, max_length=256)
|
|
22
|
+
item_kind: KnowledgeItemKind
|
|
23
|
+
feedback_kind: KnowledgeFeedbackKind
|
|
24
|
+
reason: str = Field(min_length=1, max_length=2000)
|
|
25
|
+
evidence_summary: str = Field(min_length=1, max_length=4096)
|
|
26
|
+
reported_by_kind: KnowledgeFeedbackReporterKind
|
|
27
|
+
source_uri: str | None = Field(default=None, min_length=1, max_length=2048)
|
|
28
|
+
|
|
29
|
+
@field_validator('source_uri')
|
|
30
|
+
@classmethod
|
|
31
|
+
def validate_source_uri(cls, value):
|
|
32
|
+
return _validated_source_uri(value) if value is not None else None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class KnowledgeFeedbackCreate(StrictModel):
|
|
36
|
+
personal_space_id: str = Field(min_length=1, max_length=128)
|
|
37
|
+
task_id: str = Field(min_length=1, max_length=256)
|
|
38
|
+
session_id: str | None = Field(default=None, min_length=1, max_length=256)
|
|
39
|
+
tool_name: str | None = Field(default=None, min_length=1, max_length=128)
|
|
40
|
+
items: list[KnowledgeFeedbackItem] = Field(min_length=1, max_length=200)
|
|
41
|
+
|
|
42
|
+
@model_validator(mode='after')
|
|
43
|
+
def validate_feedback_items(self):
|
|
44
|
+
identities = {
|
|
45
|
+
(item.item_kind, item.item_id, item.feedback_kind)
|
|
46
|
+
for item in self.items
|
|
47
|
+
}
|
|
48
|
+
if len(identities) != len(self.items):
|
|
49
|
+
raise ValueError('knowledge feedback items must be unique')
|
|
50
|
+
return self
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class KnowledgeFeedbackItemResult(StrictModel):
|
|
54
|
+
item_id: str
|
|
55
|
+
item_kind: KnowledgeItemKind
|
|
56
|
+
feedback_kind: KnowledgeFeedbackKind
|
|
57
|
+
recorded: bool
|
|
58
|
+
confirmation_status: ConfirmationStatus
|
|
59
|
+
utility_score: float = Field(ge=0, le=1)
|
|
60
|
+
confidence_score: float = Field(ge=0, le=1)
|
|
61
|
+
negative_evidence_count: int = Field(ge=0)
|
|
62
|
+
requires_attention: bool
|
|
63
|
+
usage_generation: int = Field(ge=1)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class KnowledgeFeedbackResult(StrictModel):
|
|
67
|
+
recorded_count: int = Field(ge=0, le=200)
|
|
68
|
+
duplicate_count: int = Field(ge=0, le=200)
|
|
69
|
+
items: list[KnowledgeFeedbackItemResult] = Field(max_length=200)
|
|
@@ -390,6 +390,12 @@ async def _read_item(store, space: dict, item_id: str, item_kind: str):
|
|
|
390
390
|
AS distinct_task_count,
|
|
391
391
|
item.fuli_last_used_at AS last_used_at,
|
|
392
392
|
coalesce(item.fuli_usage_generation, 1) AS usage_generation,
|
|
393
|
+
coalesce(item.fuli_negative_evidence_count, 0)
|
|
394
|
+
AS negative_evidence_count,
|
|
395
|
+
coalesce(item.fuli_requires_attention, false)
|
|
396
|
+
AS requires_attention,
|
|
397
|
+
item.fuli_last_feedback_kind AS last_feedback_kind,
|
|
398
|
+
item.fuli_last_feedback_at AS last_feedback_at,
|
|
393
399
|
item.fuli_replaced_by_item_id AS replaced_by_item_id,
|
|
394
400
|
item.fuli_replaced_by_item_kind AS replaced_by_item_kind
|
|
395
401
|
'''
|
|
@@ -426,6 +432,12 @@ async def _read_item(store, space: dict, item_id: str, item_kind: str):
|
|
|
426
432
|
AS distinct_task_count,
|
|
427
433
|
item.fuli_last_used_at AS last_used_at,
|
|
428
434
|
coalesce(item.fuli_usage_generation, 1) AS usage_generation,
|
|
435
|
+
coalesce(item.fuli_negative_evidence_count, 0)
|
|
436
|
+
AS negative_evidence_count,
|
|
437
|
+
coalesce(item.fuli_requires_attention, false)
|
|
438
|
+
AS requires_attention,
|
|
439
|
+
item.fuli_last_feedback_kind AS last_feedback_kind,
|
|
440
|
+
item.fuli_last_feedback_at AS last_feedback_at,
|
|
429
441
|
item.fuli_replaced_by_item_id AS replaced_by_item_id,
|
|
430
442
|
item.fuli_replaced_by_item_kind AS replaced_by_item_kind
|
|
431
443
|
'''
|
|
@@ -525,6 +537,12 @@ def _snapshot(value: dict, item_kind: str) -> dict:
|
|
|
525
537
|
'distinctTaskCount': int(value.get('distinct_task_count') or 0),
|
|
526
538
|
'lastUsedAt': _iso(value.get('last_used_at')),
|
|
527
539
|
'usageGeneration': int(value.get('usage_generation') or 1),
|
|
540
|
+
'negativeEvidenceCount': int(
|
|
541
|
+
value.get('negative_evidence_count') or 0
|
|
542
|
+
),
|
|
543
|
+
'requiresAttention': value.get('requires_attention') is True,
|
|
544
|
+
'lastFeedbackKind': value.get('last_feedback_kind'),
|
|
545
|
+
'lastFeedbackAt': _iso(value.get('last_feedback_at')),
|
|
528
546
|
'replacedByItemId': value.get('replaced_by_item_id'),
|
|
529
547
|
'replacedByItemKind': value.get('replaced_by_item_kind'),
|
|
530
548
|
}
|
|
@@ -610,6 +628,11 @@ def _next_snapshot(previous: dict, request: KnowledgeRevisionCreate, changed_at:
|
|
|
610
628
|
value['qualifiedUseCount'] = 0
|
|
611
629
|
value['distinctTaskCount'] = 0
|
|
612
630
|
value['lastUsedAt'] = None
|
|
631
|
+
if content_changed or classification_changed:
|
|
632
|
+
value['negativeEvidenceCount'] = 0
|
|
633
|
+
value['requiresAttention'] = False
|
|
634
|
+
value['lastFeedbackKind'] = None
|
|
635
|
+
value['lastFeedbackAt'] = None
|
|
613
636
|
if request.reasoning_summary is not None:
|
|
614
637
|
value['reasoningSummary'] = request.reasoning_summary or None
|
|
615
638
|
if request.profile_aspect is not None:
|
|
@@ -691,6 +714,10 @@ async def _update_item(store, space, item_id, item_kind, value, embedding):
|
|
|
691
714
|
item.fuli_distinct_task_count = $distinct_task_count,
|
|
692
715
|
item.fuli_last_used_at = $last_used_at,
|
|
693
716
|
item.fuli_usage_generation = $usage_generation,
|
|
717
|
+
item.fuli_negative_evidence_count = $negative_evidence_count,
|
|
718
|
+
item.fuli_requires_attention = $requires_attention,
|
|
719
|
+
item.fuli_last_feedback_kind = $last_feedback_kind,
|
|
720
|
+
item.fuli_last_feedback_at = $last_feedback_at,
|
|
694
721
|
item.fuli_replaced_by_item_id = $replaced_by_item_id,
|
|
695
722
|
item.fuli_replaced_by_item_kind = $replaced_by_item_kind,
|
|
696
723
|
item.name_embedding = coalesce($embedding, item.name_embedding)
|
|
@@ -718,6 +745,10 @@ async def _update_item(store, space, item_id, item_kind, value, embedding):
|
|
|
718
745
|
item.fuli_distinct_task_count = $distinct_task_count,
|
|
719
746
|
item.fuli_last_used_at = $last_used_at,
|
|
720
747
|
item.fuli_usage_generation = $usage_generation,
|
|
748
|
+
item.fuli_negative_evidence_count = $negative_evidence_count,
|
|
749
|
+
item.fuli_requires_attention = $requires_attention,
|
|
750
|
+
item.fuli_last_feedback_kind = $last_feedback_kind,
|
|
751
|
+
item.fuli_last_feedback_at = $last_feedback_at,
|
|
721
752
|
item.fuli_replaced_by_item_id = $replaced_by_item_id,
|
|
722
753
|
item.fuli_replaced_by_item_kind = $replaced_by_item_kind,
|
|
723
754
|
item.fact_embedding = coalesce($embedding, item.fact_embedding)
|
|
@@ -752,6 +783,12 @@ async def _update_item(store, space, item_id, item_kind, value, embedding):
|
|
|
752
783
|
distinct_task_count=int(value.get('distinctTaskCount') or 0),
|
|
753
784
|
last_used_at=_parse_datetime(value.get('lastUsedAt')),
|
|
754
785
|
usage_generation=int(value.get('usageGeneration') or 1),
|
|
786
|
+
negative_evidence_count=int(
|
|
787
|
+
value.get('negativeEvidenceCount') or 0
|
|
788
|
+
),
|
|
789
|
+
requires_attention=value.get('requiresAttention') is True,
|
|
790
|
+
last_feedback_kind=value.get('lastFeedbackKind'),
|
|
791
|
+
last_feedback_at=_parse_datetime(value.get('lastFeedbackAt')),
|
|
755
792
|
replaced_by_item_id=value.get('replacedByItemId'),
|
|
756
793
|
replaced_by_item_kind=value.get('replacedByItemKind'),
|
|
757
794
|
embedding=embedding,
|
|
@@ -16,6 +16,7 @@ from .search_projection import (
|
|
|
16
16
|
fact_result,
|
|
17
17
|
is_default_retrievable,
|
|
18
18
|
read_edge_epistemic_metadata,
|
|
19
|
+
source_uris_from_references,
|
|
19
20
|
)
|
|
20
21
|
|
|
21
22
|
MIN_TERM_COVERAGE = 0.25
|
|
@@ -390,7 +391,20 @@ async def _personal_entities(
|
|
|
390
391
|
AS qualified_use_count,
|
|
391
392
|
coalesce(node.fuli_distinct_task_count, 0)
|
|
392
393
|
AS distinct_task_count,
|
|
393
|
-
node.fuli_last_used_at AS last_used_at
|
|
394
|
+
node.fuli_last_used_at AS last_used_at,
|
|
395
|
+
coalesce(node.fuli_negative_evidence_count, 0)
|
|
396
|
+
AS negative_evidence_count,
|
|
397
|
+
coalesce(node.fuli_requires_attention, false)
|
|
398
|
+
AS requires_attention,
|
|
399
|
+
node.fuli_last_feedback_kind AS last_feedback_kind,
|
|
400
|
+
node.fuli_last_feedback_at AS last_feedback_at,
|
|
401
|
+
[episode IN episodes
|
|
402
|
+
WHERE episode.fuli_source_uri IS NOT NULL |
|
|
403
|
+
{
|
|
404
|
+
uri: episode.fuli_source_uri,
|
|
405
|
+
reference_time: episode.valid_at,
|
|
406
|
+
created_at: episode.created_at
|
|
407
|
+
}] AS source_references
|
|
394
408
|
ORDER BY node.name, node.uuid
|
|
395
409
|
LIMIT $candidate_limit
|
|
396
410
|
''',
|
|
@@ -565,12 +579,14 @@ def _ranked_relevance(base_score: float, metadata) -> float:
|
|
|
565
579
|
confidence_factor = 0.75 + min(max(confidence, 0), 1) * 0.25
|
|
566
580
|
utility_factor = 0.95 + min(max(utility, 0), 1) * 0.1
|
|
567
581
|
scope_factor = max(0.65, 1.0 - scope_distance * 0.15)
|
|
582
|
+
attention_factor = 0.6 if metadata.get('requires_attention') else 1.0
|
|
568
583
|
return round(
|
|
569
584
|
base_score
|
|
570
585
|
* status_factor
|
|
571
586
|
* confidence_factor
|
|
572
587
|
* utility_factor
|
|
573
|
-
* scope_factor
|
|
588
|
+
* scope_factor
|
|
589
|
+
* attention_factor,
|
|
574
590
|
4,
|
|
575
591
|
)
|
|
576
592
|
|
|
@@ -618,6 +634,7 @@ def _dedupe_ranked_entities(ranked, active_project_id):
|
|
|
618
634
|
def _entity_search_result(record, score: float, space_id: str) -> EntitySearchResult:
|
|
619
635
|
value = dict(record)
|
|
620
636
|
basis = json_object(value.pop('confirmation_basis_json', None))
|
|
637
|
+
source_references = value.pop('source_references', [])
|
|
621
638
|
created_at = _native_datetime(value.pop('created_at', None))
|
|
622
639
|
last_human_changed_at = _native_datetime(
|
|
623
640
|
value.pop('last_human_changed_at', None)
|
|
@@ -629,6 +646,9 @@ def _entity_search_result(record, score: float, space_id: str) -> EntitySearchRe
|
|
|
629
646
|
value.pop('last_agent_reviewed_at', None)
|
|
630
647
|
)
|
|
631
648
|
last_used_at = _native_datetime(value.pop('last_used_at', None))
|
|
649
|
+
last_feedback_at = _native_datetime(
|
|
650
|
+
value.pop('last_feedback_at', None)
|
|
651
|
+
)
|
|
632
652
|
for field in (
|
|
633
653
|
'assignment_project_id',
|
|
634
654
|
'episode_project_ids',
|
|
@@ -645,9 +665,11 @@ def _entity_search_result(record, score: float, space_id: str) -> EntitySearchRe
|
|
|
645
665
|
last_agent_viewed_at=last_agent_viewed_at,
|
|
646
666
|
last_agent_reviewed_at=last_agent_reviewed_at,
|
|
647
667
|
last_used_at=last_used_at,
|
|
668
|
+
last_feedback_at=last_feedback_at,
|
|
648
669
|
confirmation_basis=(
|
|
649
670
|
ConfirmationBasis.model_validate(basis) if basis else None
|
|
650
671
|
),
|
|
672
|
+
source_uris=source_uris_from_references(source_references),
|
|
651
673
|
score=score,
|
|
652
674
|
)
|
|
653
675
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
2
|
from typing import Any, Literal
|
|
3
|
+
from urllib.parse import urlsplit
|
|
3
4
|
|
|
4
5
|
from pydantic import (
|
|
5
6
|
AliasChoices,
|
|
@@ -59,8 +60,15 @@ KnowledgeAuditAction = Literal[
|
|
|
59
60
|
'agent_view',
|
|
60
61
|
'agent_review',
|
|
61
62
|
'knowledge_used',
|
|
63
|
+
'knowledge_feedback',
|
|
62
64
|
]
|
|
63
65
|
KnowledgeUseKind = Literal['cited', 'applied']
|
|
66
|
+
KnowledgeFeedbackKind = Literal[
|
|
67
|
+
'rejected',
|
|
68
|
+
'validation_failed',
|
|
69
|
+
'contradicted',
|
|
70
|
+
'outdated',
|
|
71
|
+
]
|
|
64
72
|
KnowledgeInheritanceMode = Literal[
|
|
65
73
|
'local_only',
|
|
66
74
|
'descendants',
|
|
@@ -74,6 +82,8 @@ KnowledgeRevisionAction = Literal[
|
|
|
74
82
|
'restore',
|
|
75
83
|
'scope_change',
|
|
76
84
|
'batch_confirm',
|
|
85
|
+
'promote_common',
|
|
86
|
+
'replace_common_duplicate',
|
|
77
87
|
]
|
|
78
88
|
KnowledgeContentRevisionAction = Literal[
|
|
79
89
|
'confirm',
|
|
@@ -103,10 +113,39 @@ PreferenceConflictResolutionAction = Literal[
|
|
|
103
113
|
]
|
|
104
114
|
SourceApplication = Literal['codex', 'claude_code', 'cursor', 'kiro', 'other']
|
|
105
115
|
|
|
116
|
+
|
|
106
117
|
class StrictModel(BaseModel):
|
|
107
118
|
model_config = ConfigDict(extra='forbid', str_strip_whitespace=True)
|
|
108
119
|
|
|
109
120
|
|
|
121
|
+
def _validated_source_uri(value: str) -> str:
|
|
122
|
+
if any(
|
|
123
|
+
character.isspace() or ord(character) < 32 or ord(character) == 127
|
|
124
|
+
for character in value
|
|
125
|
+
):
|
|
126
|
+
raise ValueError(
|
|
127
|
+
'source_uri must be an absolute HTTP(S) URI without credentials'
|
|
128
|
+
)
|
|
129
|
+
try:
|
|
130
|
+
parsed = urlsplit(value)
|
|
131
|
+
hostname = parsed.hostname
|
|
132
|
+
except ValueError as error:
|
|
133
|
+
raise ValueError(
|
|
134
|
+
'source_uri must be an absolute HTTP(S) URI without credentials'
|
|
135
|
+
) from error
|
|
136
|
+
if (
|
|
137
|
+
parsed.scheme.casefold() not in {'http', 'https'}
|
|
138
|
+
or not parsed.netloc
|
|
139
|
+
or not hostname
|
|
140
|
+
or parsed.username is not None
|
|
141
|
+
or parsed.password is not None
|
|
142
|
+
):
|
|
143
|
+
raise ValueError(
|
|
144
|
+
'source_uri must be an absolute HTTP(S) URI without credentials'
|
|
145
|
+
)
|
|
146
|
+
return value
|
|
147
|
+
|
|
148
|
+
|
|
110
149
|
def _validate_inheritance(
|
|
111
150
|
mode: KnowledgeInheritanceMode,
|
|
112
151
|
project_ids: list[str],
|
|
@@ -501,6 +540,7 @@ class StructuredEpisode(StrictModel):
|
|
|
501
540
|
name: str = Field(min_length=1, max_length=512)
|
|
502
541
|
source_kind: str = Field(min_length=1, max_length=128)
|
|
503
542
|
source_description: str = Field(min_length=1, max_length=1024)
|
|
543
|
+
source_uri: str | None = Field(default=None, min_length=1, max_length=2048)
|
|
504
544
|
source_application: SourceApplication | None = None
|
|
505
545
|
source_turn_id: str | None = Field(default=None, min_length=1, max_length=256)
|
|
506
546
|
source_excerpt: str | None = Field(default=None, min_length=1, max_length=2048)
|
|
@@ -510,6 +550,11 @@ class StructuredEpisode(StrictModel):
|
|
|
510
550
|
entities: list[EntityInput] = Field(min_length=1, max_length=128)
|
|
511
551
|
relationships: list[RelationshipInput] = Field(default_factory=list, max_length=256)
|
|
512
552
|
|
|
553
|
+
@field_validator('source_uri')
|
|
554
|
+
@classmethod
|
|
555
|
+
def validate_source_uri(cls, value: str | None) -> str | None:
|
|
556
|
+
return _validated_source_uri(value) if value is not None else None
|
|
557
|
+
|
|
513
558
|
@model_validator(mode='after')
|
|
514
559
|
def complete_confirmation_state(self):
|
|
515
560
|
proposer = ConfirmationActor(
|
|
@@ -722,9 +767,14 @@ class EntitySearchResult(StrictModel):
|
|
|
722
767
|
qualified_use_count: int = Field(default=0, ge=0)
|
|
723
768
|
distinct_task_count: int = Field(default=0, ge=0)
|
|
724
769
|
last_used_at: datetime | None = None
|
|
770
|
+
negative_evidence_count: int = Field(default=0, ge=0)
|
|
771
|
+
requires_attention: bool = False
|
|
772
|
+
last_feedback_kind: KnowledgeFeedbackKind | None = None
|
|
773
|
+
last_feedback_at: datetime | None = None
|
|
725
774
|
scope_distance: int = Field(default=0, ge=0, le=8)
|
|
726
775
|
inherited_from_project_id: str | None = None
|
|
727
776
|
scope_path: list[str] = Field(default_factory=list, max_length=9)
|
|
777
|
+
source_uris: list[str] = Field(default_factory=list, max_length=20)
|
|
728
778
|
score: float | None = None
|
|
729
779
|
|
|
730
780
|
|
|
@@ -764,9 +814,14 @@ class FactResult(StrictModel):
|
|
|
764
814
|
qualified_use_count: int = Field(default=0, ge=0)
|
|
765
815
|
distinct_task_count: int = Field(default=0, ge=0)
|
|
766
816
|
last_used_at: datetime | None = None
|
|
817
|
+
negative_evidence_count: int = Field(default=0, ge=0)
|
|
818
|
+
requires_attention: bool = False
|
|
819
|
+
last_feedback_kind: KnowledgeFeedbackKind | None = None
|
|
820
|
+
last_feedback_at: datetime | None = None
|
|
767
821
|
scope_distance: int = Field(default=0, ge=0, le=8)
|
|
768
822
|
inherited_from_project_id: str | None = None
|
|
769
823
|
scope_path: list[str] = Field(default_factory=list, max_length=9)
|
|
824
|
+
source_uris: list[str] = Field(default_factory=list, max_length=20)
|
|
770
825
|
score: float | None = None
|
|
771
826
|
|
|
772
827
|
|
|
@@ -892,6 +947,7 @@ class GraphEvidence(StrictModel):
|
|
|
892
947
|
name: str
|
|
893
948
|
source_description: str
|
|
894
949
|
source_kind: str
|
|
950
|
+
source_uri: str | None = Field(default=None, min_length=1, max_length=2048)
|
|
895
951
|
summary: str
|
|
896
952
|
session_id: str | None = None
|
|
897
953
|
source_application: SourceApplication | None = None
|
|
@@ -901,6 +957,11 @@ class GraphEvidence(StrictModel):
|
|
|
901
957
|
reference_time: datetime | None = None
|
|
902
958
|
created_at: datetime | None = None
|
|
903
959
|
|
|
960
|
+
@field_validator('source_uri')
|
|
961
|
+
@classmethod
|
|
962
|
+
def validate_source_uri(cls, value: str | None) -> str | None:
|
|
963
|
+
return _validated_source_uri(value) if value is not None else None
|
|
964
|
+
|
|
904
965
|
|
|
905
966
|
class KnowledgeRevisionCreate(StrictModel):
|
|
906
967
|
personal_space_id: str = Field(min_length=1, max_length=128)
|
|
@@ -1100,6 +1161,14 @@ class KnowledgeAuditRecord(StrictModel):
|
|
|
1100
1161
|
task_id: str | None = None
|
|
1101
1162
|
session_id: str | None = None
|
|
1102
1163
|
use_kind: KnowledgeUseKind | None = None
|
|
1164
|
+
feedback_kind: KnowledgeFeedbackKind | None = None
|
|
1165
|
+
reported_by_kind: Literal[
|
|
1166
|
+
'user',
|
|
1167
|
+
'agent',
|
|
1168
|
+
'authoritative_source',
|
|
1169
|
+
] | None = None
|
|
1170
|
+
evidence_summary: str | None = None
|
|
1171
|
+
source_uri: str | None = None
|
|
1103
1172
|
usage_generation: int | None = Field(default=None, ge=1)
|
|
1104
1173
|
conflict_check: Literal['no_conflict', 'conflict'] | None = None
|
|
1105
1174
|
classification_check: Literal['reasonable', 'needs_change'] | None = None
|