@tasksai/install 0.1.38 → 0.1.40

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.
Files changed (74) hide show
  1. package/README.md +34 -0
  2. package/bootstrap/Install RealtorTasksAI.command +37 -0
  3. package/bootstrap/Install RealtorTasksAI.ps1 +42 -0
  4. package/package.json +14 -3
  5. package/runtime/document_renderer.py +882 -0
  6. package/runtime/server.py +55 -581
  7. package/runtime/software_use.py +39 -0
  8. package/runtime/workflow-requirements.txt +5 -0
  9. package/runtime/workflows/__init__.py +0 -0
  10. package/runtime/workflows/account_snapshot.py +33 -0
  11. package/runtime/workflows/attachments.py +45 -0
  12. package/runtime/workflows/authority_guides.py +81 -0
  13. package/runtime/workflows/catalog.py +51 -0
  14. package/runtime/workflows/catalog_app.py +174 -0
  15. package/runtime/workflows/catalog_generation.py +186 -0
  16. package/runtime/workflows/catalog_output.py +312 -0
  17. package/runtime/workflows/catalog_suggestions.py +51 -0
  18. package/runtime/workflows/catalog_workspace.py +152 -0
  19. package/runtime/workflows/cli.py +66 -0
  20. package/runtime/workflows/document_answers.py +96 -0
  21. package/runtime/workflows/document_selection.py +50 -0
  22. package/runtime/workflows/documents.py +243 -0
  23. package/runtime/workflows/embedded/catalog.html +83 -0
  24. package/runtime/workflows/embedded/offer-review.html +516 -0
  25. package/runtime/workflows/embedded/preview.html +36 -0
  26. package/runtime/workflows/embedded_actions.py +96 -0
  27. package/runtime/workflows/embedded_demo.py +424 -0
  28. package/runtime/workflows/folders.py +120 -0
  29. package/runtime/workflows/gateway.py +157 -0
  30. package/runtime/workflows/generation_lock.py +26 -0
  31. package/runtime/workflows/launcher.py +61 -0
  32. package/runtime/workflows/licensed_delivery.py +46 -0
  33. package/runtime/workflows/mcp_dev.py +52 -0
  34. package/runtime/workflows/meeting_plan.py +92 -0
  35. package/runtime/workflows/model_client.py +26 -0
  36. package/runtime/workflows/numeric_consistency.py +72 -0
  37. package/runtime/workflows/public_source_fetch.py +66 -0
  38. package/runtime/workflows/realtor/__init__.py +0 -0
  39. package/runtime/workflows/realtor/adapter.py +179 -0
  40. package/runtime/workflows/realtor/seller_offer/ORIGIN.json +16 -0
  41. package/runtime/workflows/realtor/seller_offer/__init__.py +0 -0
  42. package/runtime/workflows/realtor/seller_offer/examples/demo-input.json +414 -0
  43. package/runtime/workflows/realtor/seller_offer/examples/make_demo.py +44 -0
  44. package/runtime/workflows/realtor/seller_offer/references/input-contract.md +65 -0
  45. package/runtime/workflows/realtor/seller_offer/references/source-boundaries.md +16 -0
  46. package/runtime/workflows/realtor/seller_offer/scripts/__init__.py +0 -0
  47. package/runtime/workflows/realtor/seller_offer/scripts/build_workbook.mjs +233 -0
  48. package/runtime/workflows/realtor/seller_offer/scripts/build_workbook.py +163 -0
  49. package/runtime/workflows/realtor/seller_offer/scripts/offer_engine.py +370 -0
  50. package/runtime/workflows/realtor/seller_offer/scripts/presentation.py +52 -0
  51. package/runtime/workflows/realtor/seller_offer/scripts/render_outputs.py +228 -0
  52. package/runtime/workflows/realtor/seller_offer/scripts/run_package.py +95 -0
  53. package/runtime/workflows/realtor/seller_offer/tests/test_engine.py +252 -0
  54. package/runtime/workflows/realtor/seller_offer/tests/test_workbook.mjs +28 -0
  55. package/runtime/workflows/realtor-release-registry.json +705 -0
  56. package/runtime/workflows/released_catalog.py +91 -0
  57. package/runtime/workflows/source_capture.py +82 -0
  58. package/runtime/workflows/store.py +492 -0
  59. package/runtime/workflows/table_calculations.py +132 -0
  60. package/runtime/workflows/template.py +65 -0
  61. package/runtime/workflows/workspace_launch.py +76 -0
  62. package/src/index.js +217 -118
  63. package/src/managed-python.js +63 -0
  64. package/src/operation-lock.js +22 -0
  65. package/src/prepared-update.js +86 -0
  66. package/src/private-workflow.js +116 -0
  67. package/src/python-runtime.js +50 -0
  68. package/src/recover-installation.js +30 -0
  69. package/src/recovery-lock.js +30 -0
  70. package/src/software-hash.js +24 -0
  71. package/src/software-use.js +32 -0
  72. package/src/update-journal.js +24 -0
  73. package/src/update-recovery.js +72 -0
  74. package/src/workspace-runtime.js +45 -0
@@ -0,0 +1,91 @@
1
+ """Source-bound intake for the 175 released Realtor skills.
2
+
3
+ The installed registry contains public identifiers and hashes only. Licensed
4
+ content must already have been delivered by the existing execute operation.
5
+ This module neither fetches content nor consumes a credit.
6
+ """
7
+ from hashlib import sha256
8
+ import json
9
+ from pathlib import Path
10
+ import re
11
+ from .store import JobError
12
+
13
+ REGISTRY = Path(__file__).with_name('realtor-release-registry.json')
14
+
15
+
16
+ def registry():
17
+ data = json.loads(REGISTRY.read_text(encoding="utf-8"))
18
+ if data.get('product_id') != 'realtor' or len(data.get('skills', {})) != 175:
19
+ raise JobError('Realtor workflow registry is invalid')
20
+ return data['skills']
21
+
22
+
23
+ def release_matches(skill_id, version, content_hash):
24
+ entry=registry().get(skill_id)
25
+ if not entry: return False
26
+ return any(r.get('version')==version and r.get('content_sha256')==content_hash
27
+ for r in [entry]+entry.get('history',[]))
28
+
29
+
30
+ def explicit_requirement(group, instruction):
31
+ """Use explicit intake labels only; never infer necessity from subject words."""
32
+ heading=re.sub(r'[*#:]', '', group).strip().casefold()
33
+ if re.match(r'^conditionally required(?: inputs| information)?$',heading):return 'conditional'
34
+ if re.match(r'^optional(?: inputs| information)?$',heading):return 'optional'
35
+ if re.match(r'^required(?: inputs| information)?$',heading):return 'required'
36
+ if re.search(r'\(optional\)(?:\s*:|\s*$)',instruction,re.I):return 'optional'
37
+ return 'evaluate_in_context'
38
+
39
+
40
+ def bind(skill_id, version, content):
41
+ content_hash=sha256(content.encode()).hexdigest() if isinstance(content,str) else None
42
+ if not release_matches(skill_id,version,content_hash):
43
+ raise JobError('This released skill needs a matching workspace conversion')
44
+ headings = list(re.finditer(r'^## (.+)$', content, re.M))
45
+ sections = {match.group(1): content[match.end():headings[i+1].start() if i+1<len(headings) else len(content)].strip()
46
+ for i,match in enumerate(headings)}
47
+ required = ('Input Format', 'Output Format', 'Source and Assumption Rules', 'Instructions', 'Quality Checks', 'Compliance and Escalation')
48
+ if any(not sections.get(key) for key in required):
49
+ raise JobError('Released skill is missing a workspace instruction section')
50
+ # Keep section labels verbatim: many carry important conditional scope.
51
+ # An input's necessity is decided by the full skill for this user request,
52
+ # not by absence of the word "optional" in a single list item.
53
+ fields = []
54
+ standalone = []
55
+ group = ''
56
+ for line in sections['Input Format'].splitlines():
57
+ if re.match(r'^#{3,6}\s+', line):
58
+ group = re.sub(r'^#{3,6}\s+', '', line)
59
+ elif re.match(r'^\*\*[^*]+\*\*', line):
60
+ if re.match(r'^\*\*[^*]+\*\*\s*(?:\([^)]*\))?\s*:\s*\S', line):
61
+ standalone.append((line, group))
62
+ else:
63
+ group = line
64
+ elif re.match(r'^\s*(?:[-*]|\d+[.)])\s+', line):
65
+ text = re.sub(r'^\s*(?:[-*]|\d+[.)])\s+', '', line)
66
+ fields.append({'key': 'input_'+str(len(fields)+1), 'instruction': text,
67
+ 'group': group, 'requirement': explicit_requirement(group, text)})
68
+ # Append newly recognized unbulleted requests so existing saved input_N
69
+ # answers retain their original meaning. A sole prose request already used
70
+ # the whole-section fallback; preserve that established field below.
71
+ if fields:
72
+ for text, parent in standalone:
73
+ fields.append({'key':'input_'+str(len(fields)+1), 'instruction':text,
74
+ 'group':parent, 'requirement':explicit_requirement(parent,text)})
75
+ no_input = bool(re.fullmatch(r'No (?:user )?inputs? (?:is |are )?required[.!]?', sections['Input Format'], re.I))
76
+ if not fields and not no_input:
77
+ fields = [{'key':'input_1','instruction':sections['Input Format'], 'group':'', 'requirement':'evaluate_in_context'}]
78
+ return {'skill_id':skill_id, 'version':version, 'content_sha256':content_hash,
79
+ 'instructions':content, 'input_instructions':sections['Input Format'],
80
+ 'output_instructions':sections['Output Format'], 'fields':fields,
81
+ 'output_policy': {'word':True, 'excel':'requires_deliverable_mapping'},
82
+ 'readiness_policy':'full_skill_context_required'}
83
+
84
+
85
+ def intake(binding, answers):
86
+ fields = binding['fields']; keys={field['key'] for field in fields}
87
+ if not isinstance(answers,dict) or set(answers)-keys or any(not isinstance(v,str) or len(v)>10000 for v in answers.values()):
88
+ raise JobError('Answers must match the selected skill and stay within the input limit')
89
+ supplied={key:answers.get(key,'').strip() for key in keys}
90
+ return {'answers':supplied, 'unanswered':[field['key'] for field in fields if not supplied[field['key']]],
91
+ 'needs_context_check':True}
@@ -0,0 +1,82 @@
1
+ """Capture a selected guide's public source as local evidence, never as approval."""
2
+ from datetime import datetime,timezone
3
+ from hashlib import sha256
4
+ from html.parser import HTMLParser
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+ import re
9
+ import uuid
10
+ from .public_source_fetch import fetch
11
+ from .documents import Documents
12
+ from .store import JobError,canonical,file_hash
13
+
14
+ class PageText(HTMLParser):
15
+ def __init__(self):super().__init__(convert_charrefs=True);self.hidden=0;self.parts=[]
16
+ def handle_starttag(self,tag,attrs):
17
+ if tag in ('script','style','head','svg'):self.hidden+=1
18
+ if not self.hidden and tag in ('p','div','br','li','h1','h2','h3','h4','tr','section'):self.parts.append('\n')
19
+ def handle_endtag(self,tag):
20
+ if tag in ('script','style','head','svg') and self.hidden:self.hidden-=1
21
+ if not self.hidden and tag in ('p','div','li','h1','h2','h3','h4','tr','section'):self.parts.append('\n')
22
+ def handle_data(self,data):
23
+ if not self.hidden:self.parts.append(data)
24
+
25
+
26
+ def capture(workspace,job_id,guide_id,index):
27
+ view=workspace.view(job_id);guide=view['authority_guide']
28
+ if not guide or guide['id']!=guide_id or guide['refresh_needed']:raise JobError('Select a current source guide first')
29
+ sources=guide['packet']['sources']
30
+ if isinstance(index,bool) or not isinstance(index,int) or not 0<=index<len(sources):raise JobError('Choose a source from the selected guide')
31
+ source=sources[index];response=fetch(source.get('url'))
32
+ raw=response['body'];raw_hash=sha256(raw).hexdigest()
33
+ identity=sha256(canonical([guide_id,response['requested_url'],response['final_url'],raw_hash]).encode()).hexdigest()
34
+ root=workspace.store._safe(workspace.store.root/job_id/'source-captures');root.mkdir(exist_ok=True,parents=True,mode=0o700)
35
+ target=workspace.store._safe(root/identity)
36
+ if not target.exists():
37
+ staging=workspace.store._safe(root/('attempt-'+uuid.uuid4().hex));staging.mkdir(mode=0o700)
38
+ metadata={k:response[k] for k in ('requested_url','final_url','content_type')}
39
+ metadata.update(capture_id=identity,guide_id=guide_id,retrieved_at=datetime.now(timezone.utc).isoformat(),body_sha256=raw_hash,verification='retrieved_not_professionally_verified')
40
+ (staging/'original.bin').write_bytes(raw)
41
+ if response['content_type']=='application/pdf':body=raw;extension='.pdf'
42
+ else:
43
+ try:text=raw.decode('utf-8-sig')
44
+ except UnicodeDecodeError:raise JobError('Source text encoding is unsupported; add an accessible source document instead')
45
+ if response['content_type']=='text/html':
46
+ parser=PageText();parser.feed(text);text='\n'.join(line.strip() for line in ''.join(parser.parts).splitlines() if line.strip())
47
+ if not text.strip():raise JobError('No readable source text found; add an accessible document')
48
+ body=('Captured public source\nURL: '+response['final_url']+'\nRetrieved at (not the source or effective date): '+metadata['retrieved_at']+'\nRetrieval is not professional verification.\n\n'+text).encode('utf-8');extension='.txt'
49
+ filename=(re.sub(r'[^A-Za-z0-9 _-]','',source.get('title') or 'Authority source').strip()[:70] or 'Authority source')+extension
50
+ (staging/filename).write_bytes(body);metadata['document_filename']=filename;metadata['document_sha256']=sha256(body).hexdigest()
51
+ (staging/'capture.json').write_bytes((canonical(metadata)+'\n').encode('utf-8'))
52
+ for path in staging.iterdir():
53
+ path.chmod(0o600)
54
+ with path.open('r+b') as stream:os.fsync(stream.fileno())
55
+ with workspace.store._db() as db:
56
+ db.execute('BEGIN IMMEDIATE');workspace.store._job(db,job_id)
57
+ if not target.exists():staging.rename(target)
58
+ metadata=json.loads(workspace.store._safe(target/'capture.json').read_text(encoding='utf-8'))
59
+ if metadata.get('capture_id')!=identity or metadata.get('guide_id')!=guide_id or metadata.get('requested_url')!=response['requested_url'] or metadata.get('final_url')!=response['final_url']:
60
+ raise JobError('Captured source identity changed; restore the saved evidence before using it')
61
+ if Path(metadata['document_filename']).name!=metadata['document_filename'] or '\\' in metadata['document_filename']:raise JobError('Invalid captured source filename')
62
+ if metadata['body_sha256']!=raw_hash or file_hash(workspace.store._safe(target/'original.bin'))!=raw_hash or file_hash(workspace.store._safe(target/metadata['document_filename']))!=metadata['document_sha256']:
63
+ raise JobError('Captured source changed; restore the saved evidence before using it')
64
+ receipt=Documents(workspace.store,target,allow_customer_documents=True).import_document(job_id,metadata['document_filename'],'source-'+identity,False)
65
+ with workspace.store._db() as db:
66
+ db.execute('CREATE TABLE IF NOT EXISTS source_captures (job_id TEXT,document_id TEXT,capture_id TEXT,metadata TEXT,PRIMARY KEY(job_id,capture_id))')
67
+ previous=db.execute('SELECT metadata FROM source_captures WHERE job_id=? AND capture_id=?',(job_id,identity)).fetchone()
68
+ if previous and previous['metadata']!=canonical(metadata):raise JobError('Captured source metadata changed; restore the saved evidence before using it')
69
+ db.execute('INSERT OR IGNORE INTO source_captures VALUES (?,?,?,?)',(job_id,receipt['document_id'],identity,canonical(metadata)))
70
+ return {**receipt,'capture':metadata}
71
+
72
+
73
+ def provenance(workspace,job_id,document_id):
74
+ with workspace.store._db() as db:
75
+ workspace.store._job(db,job_id)
76
+ if not db.execute("SELECT 1 FROM sqlite_master WHERE type='table' AND name='source_captures'").fetchone():return []
77
+ records=[json.loads(row['metadata']) for row in db.execute('SELECT metadata FROM source_captures WHERE job_id=? AND document_id=?',(job_id,document_id))]
78
+ for record in records:
79
+ folder=workspace.store._safe(workspace.store.root/job_id/'source-captures'/record['capture_id'])
80
+ if json.loads(workspace.store._safe(folder/'capture.json').read_text(encoding='utf-8'))!=record or file_hash(workspace.store._safe(folder/'original.bin'))!=record['body_sha256']:
81
+ raise JobError('Captured source evidence changed; restore it before generating')
82
+ return records