Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 405ceb7684 | |||
| 2569003f33 |
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ class AlertCalculator
|
||||
->where(['status!=' => self::CLOSED_STATUSES, 'deleted' => false])->count();
|
||||
|
||||
$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')
|
||||
->where(['cNextHearing>=' => $today, 'cNextHearing<=' => $weekEnd, 'status!=' => self::CLOSED_STATUSES, 'deleted' => false])->count();
|
||||
@@ -106,7 +106,7 @@ class AlertCalculator
|
||||
|
||||
$tasks = $this->entityManager->getRDBRepository('Task')
|
||||
->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();
|
||||
|
||||
foreach ($tasks as $task) {
|
||||
@@ -132,7 +132,7 @@ class AlertCalculator
|
||||
$recentTaskThreshold = date('Y-m-d H:i:s', strtotime('-7 days'));
|
||||
|
||||
$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])
|
||||
->order('cNextHearing', 'ASC')->find();
|
||||
|
||||
@@ -140,13 +140,7 @@ class AlertCalculator
|
||||
$lastActivity = $case->get('cLastActivityAt');
|
||||
if ($lastActivity && $lastActivity >= $recentActivityThreshold) continue;
|
||||
|
||||
$openTaskCount = $this->entityManager->getRDBRepository('Task')
|
||||
->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;
|
||||
if ($this->caseHasPreparation($case, $recentTaskThreshold)) continue;
|
||||
|
||||
$daysUntil = (new \DateTime())->diff(new \DateTime($case->get('cNextHearing')))->days;
|
||||
$severity = ($daysUntil <= 1) ? 'critical' : 'warning';
|
||||
@@ -162,6 +156,104 @@ class AlertCalculator
|
||||
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
|
||||
{
|
||||
$alerts = [];
|
||||
@@ -191,7 +283,7 @@ class AlertCalculator
|
||||
|
||||
$tasks = $this->entityManager->getRDBRepository('Task')
|
||||
->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();
|
||||
|
||||
foreach ($tasks as $task) {
|
||||
@@ -237,7 +329,7 @@ class AlertCalculator
|
||||
|
||||
foreach (array_keys($userCounts) as $uid) {
|
||||
$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);
|
||||
|
||||
@@ -82,9 +82,18 @@ class CaseContextBuilder
|
||||
|
||||
private function getOpenTasks(string $caseId): array
|
||||
{
|
||||
$parentConditions = [
|
||||
['parentType' => 'Case', 'parentId' => $caseId],
|
||||
];
|
||||
|
||||
$contactIds = $this->getCaseContactIds($caseId);
|
||||
if (!empty($contactIds)) {
|
||||
$parentConditions[] = ['parentType' => 'Contact', 'parentId' => $contactIds];
|
||||
}
|
||||
|
||||
$tasks = [];
|
||||
$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();
|
||||
|
||||
foreach ($collection as $t) {
|
||||
@@ -97,6 +106,32 @@ class CaseContextBuilder
|
||||
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
|
||||
{
|
||||
$meetings = [];
|
||||
|
||||
@@ -58,10 +58,18 @@ class OfficeContextBuilder
|
||||
$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 = [];
|
||||
foreach ($this->entityManager->getRDBRepository('Task')
|
||||
->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) {
|
||||
$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];
|
||||
}
|
||||
|
||||
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",
|
||||
"description": "Unified AI Assistant for Legal CRM — floating chat with case memory, office alerts, and AI Gateway integration",
|
||||
"author": "klear",
|
||||
"version": "2.7.2",
|
||||
"version": "2.7.3",
|
||||
"acceptableVersions": [
|
||||
">=8.0.0"
|
||||
],
|
||||
"releaseDate": "2026-04-09",
|
||||
"releaseDate": "2026-04-14",
|
||||
"php": [
|
||||
">=8.1"
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user