Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 405ceb7684 | |||
| 2569003f33 | |||
| 289a9cf27f |
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"models": {
|
||||||
|
"main": {
|
||||||
|
"provider": "anthropic",
|
||||||
|
"modelId": "claude-sonnet-4-20250514",
|
||||||
|
"maxTokens": 64000,
|
||||||
|
"temperature": 0.2,
|
||||||
|
"id": "claude-sonnet-4-20250514"
|
||||||
|
},
|
||||||
|
"research": {
|
||||||
|
"provider": "anthropic",
|
||||||
|
"modelId": "sonar",
|
||||||
|
"maxTokens": 8700,
|
||||||
|
"temperature": 0.1,
|
||||||
|
"id": "claude-sonnet-4-20250514"
|
||||||
|
},
|
||||||
|
"fallback": {
|
||||||
|
"provider": "anthropic",
|
||||||
|
"modelId": "claude-3-7-sonnet-20250219",
|
||||||
|
"maxTokens": 120000,
|
||||||
|
"temperature": 0.2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"global": {
|
||||||
|
"logLevel": "info",
|
||||||
|
"debug": false,
|
||||||
|
"defaultNumTasks": 10,
|
||||||
|
"defaultSubtasks": 5,
|
||||||
|
"defaultPriority": "medium",
|
||||||
|
"projectName": "Task Master",
|
||||||
|
"ollamaBaseURL": "http://localhost:11434/api",
|
||||||
|
"bedrockBaseURL": "https://bedrock.us-east-1.amazonaws.com",
|
||||||
|
"responseLanguage": "English",
|
||||||
|
"enableCodebaseAnalysis": true,
|
||||||
|
"enableProxy": false,
|
||||||
|
"anonymousTelemetry": true,
|
||||||
|
"userId": "1234567890"
|
||||||
|
},
|
||||||
|
"claudeCode": {},
|
||||||
|
"codexCli": {},
|
||||||
|
"grokCli": {
|
||||||
|
"timeout": 120000,
|
||||||
|
"workingDirectory": null,
|
||||||
|
"defaultModel": "grok-4-latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"currentTag": "master",
|
||||||
|
"lastSwitched": "2026-04-14T06:05:06.044Z",
|
||||||
|
"branchTagMapping": {},
|
||||||
|
"migrationNoticeShown": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"master": {
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"title": "fix: broaden task detection to include Contact-linked tasks",
|
||||||
|
"description": "AlertCalculator, CaseContextBuilder, and OfficeContextBuilder only queried tasks with parentType='Case', missing tasks linked to case contacts (parentType='Contact'). This caused false 'no preparation task' alerts in Shira's daily standup.",
|
||||||
|
"status": "in-progress",
|
||||||
|
"priority": "high",
|
||||||
|
"dependencies": [],
|
||||||
|
"details": "Root cause confirmed via production API: preparation tasks had parentType='Contact' because Shira created them linked to the contact entity. Fix broadens all task queries to include both Case and Contact parent types, plus NhActivity-linked tasks.",
|
||||||
|
"testStrategy": "Deploy to staging, trigger standup for a case with Contact-linked tasks, verify no false alert.",
|
||||||
|
"subtasks": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"created": "2026-04-14T06:05:06.042Z",
|
||||||
|
"updated": "2026-04-14T09:15:00.000Z",
|
||||||
|
"description": "Tasks for master context"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -186,7 +186,7 @@ class SmartAssistant
|
|||||||
}
|
}
|
||||||
|
|
||||||
$tool = $data->tool;
|
$tool = $data->tool;
|
||||||
$params = (array) ($data->params ?? []);
|
$params = json_decode(json_encode($data->params ?? []), true) ?? [];
|
||||||
$caseId = $data->caseId ?? null;
|
$caseId = $data->caseId ?? null;
|
||||||
|
|
||||||
$executor = $this->injectableFactory->create(ActionExecutor::class);
|
$executor = $this->injectableFactory->create(ActionExecutor::class);
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class AlertCalculator
|
|||||||
->where(['status!=' => self::CLOSED_STATUSES, 'deleted' => false])->count();
|
->where(['status!=' => self::CLOSED_STATUSES, 'deleted' => false])->count();
|
||||||
|
|
||||||
$totalOverdueTasks = $this->entityManager->getRDBRepository('Task')
|
$totalOverdueTasks = $this->entityManager->getRDBRepository('Task')
|
||||||
->where(['status!=' => ['Completed', 'Canceled', 'Deferred'], 'dateEnd<' => $today, 'dateEnd!=' => null, 'deleted' => false, 'parentType' => 'Case'])->count();
|
->where(['status!=' => ['Completed', 'Canceled', 'Deferred'], 'dateEnd<' => $today, 'dateEnd!=' => null, 'deleted' => false, 'parentType' => ['Case', 'Contact']])->count();
|
||||||
|
|
||||||
$upcomingHearings = $this->entityManager->getRDBRepository('Case')
|
$upcomingHearings = $this->entityManager->getRDBRepository('Case')
|
||||||
->where(['cNextHearing>=' => $today, 'cNextHearing<=' => $weekEnd, 'status!=' => self::CLOSED_STATUSES, 'deleted' => false])->count();
|
->where(['cNextHearing>=' => $today, 'cNextHearing<=' => $weekEnd, 'status!=' => self::CLOSED_STATUSES, 'deleted' => false])->count();
|
||||||
@@ -106,7 +106,7 @@ class AlertCalculator
|
|||||||
|
|
||||||
$tasks = $this->entityManager->getRDBRepository('Task')
|
$tasks = $this->entityManager->getRDBRepository('Task')
|
||||||
->select(['id', 'name', 'dateEnd', 'status', 'parentId', 'parentType', 'parentName', 'assignedUserName'])
|
->select(['id', 'name', 'dateEnd', 'status', 'parentId', 'parentType', 'parentName', 'assignedUserName'])
|
||||||
->where(['status!=' => ['Completed', 'Canceled', 'Deferred'], 'dateEnd<' => $today, 'dateEnd!=' => null, 'deleted' => false, 'parentType' => 'Case'])
|
->where(['status!=' => ['Completed', 'Canceled', 'Deferred'], 'dateEnd<' => $today, 'dateEnd!=' => null, 'deleted' => false, 'parentType' => ['Case', 'Contact']])
|
||||||
->order('dateEnd', 'ASC')->limit(0, 50)->find();
|
->order('dateEnd', 'ASC')->limit(0, 50)->find();
|
||||||
|
|
||||||
foreach ($tasks as $task) {
|
foreach ($tasks as $task) {
|
||||||
@@ -132,7 +132,7 @@ class AlertCalculator
|
|||||||
$recentTaskThreshold = date('Y-m-d H:i:s', strtotime('-7 days'));
|
$recentTaskThreshold = date('Y-m-d H:i:s', strtotime('-7 days'));
|
||||||
|
|
||||||
$cases = $this->entityManager->getRDBRepository('Case')
|
$cases = $this->entityManager->getRDBRepository('Case')
|
||||||
->select(['id', 'name', 'cNextHearing', 'assignedUserName', 'cLastActivityAt'])
|
->select(['id', 'name', 'cNextHearing', 'assignedUserName', 'cLastActivityAt', 'contactId'])
|
||||||
->where(['cNextHearing>=' => $today, 'cNextHearing<=' => $threshold, 'status!=' => self::CLOSED_STATUSES, 'deleted' => false])
|
->where(['cNextHearing>=' => $today, 'cNextHearing<=' => $threshold, 'status!=' => self::CLOSED_STATUSES, 'deleted' => false])
|
||||||
->order('cNextHearing', 'ASC')->find();
|
->order('cNextHearing', 'ASC')->find();
|
||||||
|
|
||||||
@@ -140,13 +140,7 @@ class AlertCalculator
|
|||||||
$lastActivity = $case->get('cLastActivityAt');
|
$lastActivity = $case->get('cLastActivityAt');
|
||||||
if ($lastActivity && $lastActivity >= $recentActivityThreshold) continue;
|
if ($lastActivity && $lastActivity >= $recentActivityThreshold) continue;
|
||||||
|
|
||||||
$openTaskCount = $this->entityManager->getRDBRepository('Task')
|
if ($this->caseHasPreparation($case, $recentTaskThreshold)) continue;
|
||||||
->where(['parentId' => $case->get('id'), 'parentType' => 'Case', 'status!=' => ['Completed', 'Canceled', 'Deferred'], 'deleted' => false])->count();
|
|
||||||
if ($openTaskCount > 0) continue;
|
|
||||||
|
|
||||||
$recentCompleted = $this->entityManager->getRDBRepository('Task')
|
|
||||||
->where(['parentId' => $case->get('id'), 'parentType' => 'Case', 'status' => 'Completed', 'modifiedAt>=' => $recentTaskThreshold, 'deleted' => false])->count();
|
|
||||||
if ($recentCompleted > 0) continue;
|
|
||||||
|
|
||||||
$daysUntil = (new \DateTime())->diff(new \DateTime($case->get('cNextHearing')))->days;
|
$daysUntil = (new \DateTime())->diff(new \DateTime($case->get('cNextHearing')))->days;
|
||||||
$severity = ($daysUntil <= 1) ? 'critical' : 'warning';
|
$severity = ($daysUntil <= 1) ? 'critical' : 'warning';
|
||||||
@@ -162,6 +156,104 @@ class AlertCalculator
|
|||||||
return $alerts;
|
return $alerts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a case has any evidence of hearing preparation:
|
||||||
|
* 1. Tasks linked directly to the case (parentType='Case')
|
||||||
|
* 2. Tasks linked to the case's contacts (parentType='Contact')
|
||||||
|
* 3. Tasks linked through NhActivity records for this case
|
||||||
|
*/
|
||||||
|
private function caseHasPreparation(\Espo\ORM\Entity $case, string $recentTaskThreshold): bool
|
||||||
|
{
|
||||||
|
$caseId = $case->get('id');
|
||||||
|
|
||||||
|
// Collect all parentId+parentType pairs that relate to this case
|
||||||
|
$parentConditions = [
|
||||||
|
['parentType' => 'Case', 'parentId' => $caseId],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Also check tasks linked to the case's contacts
|
||||||
|
$contactIds = $this->getCaseContactIds($case);
|
||||||
|
if (!empty($contactIds)) {
|
||||||
|
$parentConditions[] = ['parentType' => 'Contact', 'parentId' => $contactIds];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check 1: Any open tasks linked to the case or its contacts
|
||||||
|
$openTaskCount = $this->entityManager->getRDBRepository('Task')
|
||||||
|
->where([
|
||||||
|
'OR' => $parentConditions,
|
||||||
|
'status!=' => ['Completed', 'Canceled', 'Deferred'],
|
||||||
|
'deleted' => false,
|
||||||
|
])->count();
|
||||||
|
if ($openTaskCount > 0) return true;
|
||||||
|
|
||||||
|
// Check 2: Any recently completed tasks linked to the case or its contacts
|
||||||
|
$recentCompleted = $this->entityManager->getRDBRepository('Task')
|
||||||
|
->where([
|
||||||
|
'OR' => $parentConditions,
|
||||||
|
'status' => 'Completed',
|
||||||
|
'modifiedAt>=' => $recentTaskThreshold,
|
||||||
|
'deleted' => false,
|
||||||
|
])->count();
|
||||||
|
if ($recentCompleted > 0) return true;
|
||||||
|
|
||||||
|
// Check 3: Tasks linked through NhActivity records for this case
|
||||||
|
$nhActivityTaskCount = $this->entityManager->getRDBRepository('NhActivity')
|
||||||
|
->where([
|
||||||
|
'caseId' => $caseId,
|
||||||
|
'taskId!=' => null,
|
||||||
|
'deleted' => false,
|
||||||
|
])->count();
|
||||||
|
|
||||||
|
if ($nhActivityTaskCount > 0) {
|
||||||
|
$nhActivities = $this->entityManager->getRDBRepository('NhActivity')
|
||||||
|
->select(['taskId'])
|
||||||
|
->where([
|
||||||
|
'caseId' => $caseId,
|
||||||
|
'taskId!=' => null,
|
||||||
|
'deleted' => false,
|
||||||
|
])->find();
|
||||||
|
|
||||||
|
$taskIds = [];
|
||||||
|
foreach ($nhActivities as $nha) {
|
||||||
|
$taskIds[] = $nha->get('taskId');
|
||||||
|
}
|
||||||
|
|
||||||
|
$activeNhTasks = $this->entityManager->getRDBRepository('Task')
|
||||||
|
->where([
|
||||||
|
'id' => $taskIds,
|
||||||
|
'status!=' => ['Canceled'],
|
||||||
|
'deleted' => false,
|
||||||
|
])->count();
|
||||||
|
if ($activeNhTasks > 0) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getCaseContactIds(\Espo\ORM\Entity $case): array
|
||||||
|
{
|
||||||
|
$contactIds = [];
|
||||||
|
|
||||||
|
$primaryContactId = $case->get('contactId');
|
||||||
|
if ($primaryContactId) {
|
||||||
|
$contactIds[] = $primaryContactId;
|
||||||
|
}
|
||||||
|
|
||||||
|
$contacts = $this->entityManager->getRDBRepository('Case')
|
||||||
|
->getRelation($case, 'contacts')
|
||||||
|
->select(['id'])
|
||||||
|
->find();
|
||||||
|
|
||||||
|
foreach ($contacts as $contact) {
|
||||||
|
$id = $contact->get('id');
|
||||||
|
if (!in_array($id, $contactIds)) {
|
||||||
|
$contactIds[] = $id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $contactIds;
|
||||||
|
}
|
||||||
|
|
||||||
private function findUnassignedNewCases(): array
|
private function findUnassignedNewCases(): array
|
||||||
{
|
{
|
||||||
$alerts = [];
|
$alerts = [];
|
||||||
@@ -191,7 +283,7 @@ class AlertCalculator
|
|||||||
|
|
||||||
$tasks = $this->entityManager->getRDBRepository('Task')
|
$tasks = $this->entityManager->getRDBRepository('Task')
|
||||||
->select(['id', 'name', 'dateEnd', 'parentId', 'parentType', 'parentName', 'assignedUserName'])
|
->select(['id', 'name', 'dateEnd', 'parentId', 'parentType', 'parentName', 'assignedUserName'])
|
||||||
->where(['status!=' => ['Completed', 'Canceled', 'Deferred'], 'dateEnd>=' => $today, 'dateEnd<=' => $threshold, 'deleted' => false, 'parentType' => 'Case'])
|
->where(['status!=' => ['Completed', 'Canceled', 'Deferred'], 'dateEnd>=' => $today, 'dateEnd<=' => $threshold, 'deleted' => false, 'parentType' => ['Case', 'Contact']])
|
||||||
->order('dateEnd', 'ASC')->limit(0, 30)->find();
|
->order('dateEnd', 'ASC')->limit(0, 30)->find();
|
||||||
|
|
||||||
foreach ($tasks as $task) {
|
foreach ($tasks as $task) {
|
||||||
@@ -237,7 +329,7 @@ class AlertCalculator
|
|||||||
|
|
||||||
foreach (array_keys($userCounts) as $uid) {
|
foreach (array_keys($userCounts) as $uid) {
|
||||||
$userCounts[$uid]['openTasks'] = $this->entityManager->getRDBRepository('Task')
|
$userCounts[$uid]['openTasks'] = $this->entityManager->getRDBRepository('Task')
|
||||||
->where(['assignedUserId' => $uid, 'status!=' => ['Completed', 'Canceled', 'Deferred'], 'deleted' => false, 'parentType' => 'Case'])->count();
|
->where(['assignedUserId' => $uid, 'status!=' => ['Completed', 'Canceled', 'Deferred'], 'deleted' => false, 'parentType' => ['Case', 'Contact']])->count();
|
||||||
}
|
}
|
||||||
|
|
||||||
return array_values($userCounts);
|
return array_values($userCounts);
|
||||||
|
|||||||
@@ -82,9 +82,18 @@ class CaseContextBuilder
|
|||||||
|
|
||||||
private function getOpenTasks(string $caseId): array
|
private function getOpenTasks(string $caseId): array
|
||||||
{
|
{
|
||||||
|
$parentConditions = [
|
||||||
|
['parentType' => 'Case', 'parentId' => $caseId],
|
||||||
|
];
|
||||||
|
|
||||||
|
$contactIds = $this->getCaseContactIds($caseId);
|
||||||
|
if (!empty($contactIds)) {
|
||||||
|
$parentConditions[] = ['parentType' => 'Contact', 'parentId' => $contactIds];
|
||||||
|
}
|
||||||
|
|
||||||
$tasks = [];
|
$tasks = [];
|
||||||
$collection = $this->entityManager->getRDBRepository('Task')
|
$collection = $this->entityManager->getRDBRepository('Task')
|
||||||
->where(['parentType' => 'Case', 'parentId' => $caseId, 'status!=' => ['Completed', 'Canceled']])
|
->where(['OR' => $parentConditions, 'status!=' => ['Completed', 'Canceled']])
|
||||||
->order('dateEnd', 'ASC')->limit(0, 10)->find();
|
->order('dateEnd', 'ASC')->limit(0, 10)->find();
|
||||||
|
|
||||||
foreach ($collection as $t) {
|
foreach ($collection as $t) {
|
||||||
@@ -97,6 +106,32 @@ class CaseContextBuilder
|
|||||||
return $tasks;
|
return $tasks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getCaseContactIds(string $caseId): array
|
||||||
|
{
|
||||||
|
$case = $this->entityManager->getEntityById('Case', $caseId);
|
||||||
|
if (!$case) return [];
|
||||||
|
|
||||||
|
$contactIds = [];
|
||||||
|
$primaryContactId = $case->get('contactId');
|
||||||
|
if ($primaryContactId) {
|
||||||
|
$contactIds[] = $primaryContactId;
|
||||||
|
}
|
||||||
|
|
||||||
|
$contacts = $this->entityManager->getRDBRepository('Case')
|
||||||
|
->getRelation($case, 'contacts')
|
||||||
|
->select(['id'])
|
||||||
|
->find();
|
||||||
|
|
||||||
|
foreach ($contacts as $contact) {
|
||||||
|
$id = $contact->get('id');
|
||||||
|
if (!in_array($id, $contactIds)) {
|
||||||
|
$contactIds[] = $id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $contactIds;
|
||||||
|
}
|
||||||
|
|
||||||
private function getUpcomingMeetings(string $caseId): array
|
private function getUpcomingMeetings(string $caseId): array
|
||||||
{
|
{
|
||||||
$meetings = [];
|
$meetings = [];
|
||||||
|
|||||||
@@ -58,10 +58,18 @@ class OfficeContextBuilder
|
|||||||
$contacts[] = ['name' => $c->get('name'), 'phone' => $c->get('phoneNumber'), 'email' => $c->get('emailAddress')];
|
$contacts[] = ['name' => $c->get('name'), 'phone' => $c->get('phoneNumber'), 'email' => $c->get('emailAddress')];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$parentConditions = [
|
||||||
|
['parentType' => 'Case', 'parentId' => $caseId],
|
||||||
|
];
|
||||||
|
$contactIds = $this->getCaseContactIds($case);
|
||||||
|
if (!empty($contactIds)) {
|
||||||
|
$parentConditions[] = ['parentType' => 'Contact', 'parentId' => $contactIds];
|
||||||
|
}
|
||||||
|
|
||||||
$tasks = [];
|
$tasks = [];
|
||||||
foreach ($this->entityManager->getRDBRepository('Task')
|
foreach ($this->entityManager->getRDBRepository('Task')
|
||||||
->select(['id', 'name', 'status', 'dateEnd', 'assignedUserName'])
|
->select(['id', 'name', 'status', 'dateEnd', 'assignedUserName'])
|
||||||
->where(['parentId' => $caseId, 'parentType' => 'Case', 'status!=' => ['Completed', 'Canceled'], 'deleted' => false])
|
->where(['OR' => $parentConditions, 'status!=' => ['Completed', 'Canceled'], 'deleted' => false])
|
||||||
->order('dateEnd', 'ASC')->limit(0, 20)->find() as $t) {
|
->order('dateEnd', 'ASC')->limit(0, 20)->find() as $t) {
|
||||||
$tasks[] = ['name' => $t->get('name'), 'status' => $t->get('status'), 'dateEnd' => $t->get('dateEnd'), 'assignedUser' => $t->get('assignedUserName')];
|
$tasks[] = ['name' => $t->get('name'), 'status' => $t->get('status'), 'dateEnd' => $t->get('dateEnd'), 'assignedUser' => $t->get('assignedUserName')];
|
||||||
}
|
}
|
||||||
@@ -76,4 +84,27 @@ class OfficeContextBuilder
|
|||||||
|
|
||||||
return ['case' => $caseData, 'contacts' => $contacts, 'openTasks' => $tasks, 'recentNotes' => $notes];
|
return ['case' => $caseData, 'contacts' => $contacts, 'openTasks' => $tasks, 'recentNotes' => $notes];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getCaseContactIds(\Espo\ORM\Entity $case): array
|
||||||
|
{
|
||||||
|
$contactIds = [];
|
||||||
|
$primaryContactId = $case->get('contactId');
|
||||||
|
if ($primaryContactId) {
|
||||||
|
$contactIds[] = $primaryContactId;
|
||||||
|
}
|
||||||
|
|
||||||
|
$contacts = $this->entityManager->getRDBRepository('Case')
|
||||||
|
->getRelation($case, 'contacts')
|
||||||
|
->select(['id'])
|
||||||
|
->find();
|
||||||
|
|
||||||
|
foreach ($contacts as $contact) {
|
||||||
|
$id = $contact->get('id');
|
||||||
|
if (!in_array($id, $contactIds)) {
|
||||||
|
$contactIds[] = $id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $contactIds;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -3,11 +3,11 @@
|
|||||||
"module": "SmartAssistant",
|
"module": "SmartAssistant",
|
||||||
"description": "Unified AI Assistant for Legal CRM — floating chat with case memory, office alerts, and AI Gateway integration",
|
"description": "Unified AI Assistant for Legal CRM — floating chat with case memory, office alerts, and AI Gateway integration",
|
||||||
"author": "klear",
|
"author": "klear",
|
||||||
"version": "2.7.1",
|
"version": "2.7.3",
|
||||||
"acceptableVersions": [
|
"acceptableVersions": [
|
||||||
">=8.0.0"
|
">=8.0.0"
|
||||||
],
|
],
|
||||||
"releaseDate": "2026-04-09",
|
"releaseDate": "2026-04-14",
|
||||||
"php": [
|
"php": [
|
||||||
">=8.1"
|
">=8.1"
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user