Series: Building Quorentra CRM from Zero — A Modular, ChatGPT-Native AI CRM
Building unified CRM search, entity resolution, permission-aware customer context retrieval, bounded context packages, provenance, and grounded ChatGPT answers.

1. Introduction
Quorentra has reached an important point in its modular development.
We now have six core operational CRM domains (Part 24):
CompanyContactOpportunityActivityTaskMeeting
Together they represent most of the structured information required for a useful CRM MVP.
Company→ Who are we doing business with?Contact→ Who are we talking to?Opportunity→ What are we trying to sell?Activity→ What happened?Task→ What needs to happen?Meeting→ What customer interaction is scheduled?
Each module is independently useful.
But ChatGPT rarely needs information from only one module.
Consider the question:
What’s happening with Adventure Works?
There is no single database record containing the answer.
The answer may require:
Company+Contacts+Opportunities+Recent Activities+Open Tasks+Upcoming Meetings
ChatGPT could theoretically call six different tools.
But that creates several problems.
The model must decide:
Which tools?Which filters?Which records?Which time windows?Which result limits?Which relationships?
More importantly, we would be delegating too much retrieval logic to the language model.
That is not the architecture we want.
Instead, Quorentra needs a server-owned:
CRM Context Layer
Its responsibility is to transform a CRM entity into a bounded, permission-aware, tenant-isolated package of relevant business context.
This becomes the evidence layer between the CRM database and ChatGPT.
2. Our Target Interaction
The user asks:
What’s happening with Adventure Works?
ChatGPT identifies:
Intent:Retrieve CRM ContextEntity Reference:Adventure Works
It calls something conceptually similar to:
get_crm_context( entity_reference = "Adventure Works")
Quorentra resolves:
Adventure Works ↓Company
and builds:
CRM Context│├── Company│├── Key Contacts│├── Open Opportunities│├── Recent Activities│├── Open Tasks└── Upcoming Meetings
ChatGPT receives structured evidence such as:
Adventure WorksContactsSarah JohnsonLisa ChenOpen OpportunitiesAzure Migration€90,000Proposal StageRecent ActivityCustomer requested revised pricing.Open TasksSend revised proposalDue FridayUpcoming MeetingsProposal ReviewTuesday at 14:00
ChatGPT can then answer:
Adventure Works currently has an active €90,000 Azure Migration opportunity in the Proposal stage. Sarah Johnson recently requested revised pricing, and you have an open task to send the revised proposal by Friday. A proposal review meeting is scheduled for Tuesday at 14:00.
Every important statement comes from authoritative CRM evidence.
That is the goal.
3. Why This Layer Matters
Without a Context Layer, the architecture looks like:
ChatGPT
│
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
Companies Contacts Opportunities
│ │ │
▼ ▼ ▼
Activities Tasks Meetings
ChatGPT becomes responsible for orchestrating low-level retrieval.
With a Context Layer:
ChatGPT │ ▼CRM Context Layer │ ├── Companies ├── Contacts ├── Opportunities ├── Activities ├── Tasks └── Meetings
The backend owns retrieval semantics.
That is safer and more predictable.
4. ChatGPT Should Reason Over Evidence
A useful design principle for Quorentra is:
ChatGPT should reason over CRM evidence, not construct CRM evidence.
The distinction is important.
ChatGPT can decide:
What does this information mean?What is important?How should it be explained?
Quorentra decides:
Which tenant?Which entity?Which records?Which permissions?Which relationships?Which time ranges?Which result limits?
This separation gives us grounded AI behavior.
5. The Context Layer Is Not RAG Yet
It may sound similar to Retrieval-Augmented Generation.
But Part 25 is not yet building:
EmbeddingsVector SearchSemantic ChunkingpgvectorDocument RetrievalKnowledge Base RAG
Those come later.
For now, we are retrieving structured relational CRM data.
Think of this as:
Structured CRM Retrieval
rather than:
Semantic Knowledge Retrieval
Both will eventually work together.
6. Starting Checkpoint
Before implementing Part 25, verify Part 24.
Run:
cd backendpython -m pytest
Then:
cd ..\chatgpt-uinpm run build
Test:
Schedule a meeting with Sarah Johnson at Adventure Works next Tuesday at 2 PM to review the Azure migration proposal.
Verify:
Meeting Draft ↓Participant Resolution ↓CRM Relationship Resolution ↓Time Resolution ↓Conflict Detection ↓Preparation ↓Confirmation ↓Meeting Creation
The Context Layer will reuse the same entity resolution infrastructure.
7. New Permissions
Introduce:
crm.searchcrm.context.read
These are intentionally broader orchestration permissions.
They do not replace domain permissions.
For example:
crm.context.read+companies.read+contacts.read+opportunities.read+activities.read+tasks.read+meetings.read
determines what can appear in a context package.
8. Why Domain Permissions Still Matter
Suppose a user has:
companies.readcontacts.read
but does not have:
opportunities.read
Then:
What’s happening with Adventure Works?
must not expose Opportunity information.
The context package might contain:
Company✓Contacts✓OpportunitiesomittedActivitiesdepends on permissionTasksdepends on permissionMeetingsdepends on permission
The Context Layer must never bypass domain authorization.
9. Permission Intersection
Think of context retrieval as:
Requested Context ∩Tenant Scope ∩User Permissions ∩Entity Relationships =Returned Context
This is a critical architectural rule.
10. Unified CRM Search
Before we can build context, ChatGPT needs a consistent way to find CRM entities.
Introduce:
search_crm
This searches across supported entity types.
11. Searchable Entity Types
For the MVP:
CompanyContactOpportunityTaskMeeting
Activities can also be searchable, although their primary use is usually timeline retrieval.
Conceptually:
class CRMEntityType(str, Enum): COMPANY = "company" CONTACT = "contact" OPPORTUNITY = "opportunity" ACTIVITY = "activity" TASK = "task" MEETING = "meeting"
12. Unified Search Input
Conceptually:
class SearchCRMInput(BaseModel): query: str entity_types: list[CRMEntityType] | None = None limit: int = 10
The model provides:
queryoptional entity types
The server provides:
tenant scopepermission filteringrankingresult limits
13. Example Search
User:
Find Adventure Works.
ChatGPT calls:
search_crm( query = "Adventure Works")
Quorentra may return:
1. Company Adventure Works2. Opportunity Adventure Works Azure Migration3. Task Send Adventure Works revised proposal
Each result has a type.
14. Structured Search Results
Conceptually:
{ "results": [ { "entity_type": "company", "entity_id": "...", "display_name": "Adventure Works", "secondary_text": "Technology Services", "score": 0.98 }, { "entity_type": "opportunity", "entity_id": "...", "display_name": "Azure Migration", "secondary_text": "Adventure Works", "score": 0.84 } ]}
ChatGPT does not need to know database table details.
15. Search Result IDs Are Authoritative
The model searches using human references:
Adventure WorksSarah JohnsonAzure Migration
The server returns authoritative:
entity_typeentity_id
Subsequent context calls should use those resolved identifiers where possible.
16. Search Is Tenant-Scoped
Every query automatically includes:
organization_id =TenantContext.organization_id
There is no global CRM search.
Even if another tenant contains an identical:
Adventure Works
it must never appear.
17. Search Is Permission-Aware
If the user lacks:
tasks.read
Task results should not appear.
If the user lacks:
meetings.read
Meeting results should not appear.
Search itself must respect the same authorization model as direct domain reads.
18. Search Ranking
For the MVP, ranking can remain simple.
Use signals such as:
Exact name matchPrefix matchSubstring matchEntity importanceRecency
For example:
Adventure Works
exact Company match should rank above:
Call Adventure Works Friday
Task title substring match.
19. Do Not Start with Vector Search
We do not need embeddings to search:
Adventure WorksSarah JohnsonAzure Migration
Relational and textual matching is sufficient for the MVP.
Vector search becomes useful later for:
documentsnoteslong activity contentknowledge retrievalsemantic questions
Keep the architecture modular.
20. Define a Search Result Contract
Conceptually:
class CRMSearchResult(BaseModel): entity_type: CRMEntityType entity_id: UUID display_name: str secondary_text: str | None score: float
This gives ChatGPT a consistent search surface.
21. Search Result Limits
Do not return hundreds of matches.
Default:
10
Maximum:
50
or another conservative server-controlled value.
The model should receive only useful candidates.
22. Ambiguous Search
Suppose:
Adventure Works EuropeAdventure Works ConsultingAdventure Works Cloud
The user asks:
What’s happening with Adventure Works?
Do not choose randomly.
Return candidates.
ChatGPT can ask:
Which Adventure Works do you mean?
23. Context Begins with Resolution
The Context Layer should normally operate on a resolved entity.
Flow:
Natural Language Reference ↓search_crm ↓Resolved Entity ↓get_crm_context
This cleanly separates:
Finding
from:
Context Building
24. Introduce get_crm_context
This becomes one of Quorentra’s most important ChatGPT tools.
Conceptually:
class GetCRMContextInput(BaseModel): entity_type: CRMEntityType entity_id: UUID
Later optional context controls can be added.
For the MVP, server defaults are preferable.
25. Why Use Resolved IDs?
Once Quorentra has resolved:
Adventure Works ↓company_id
do not make every subsequent query repeat fuzzy matching.
Use the authoritative ID.
This reduces ambiguity and unnecessary database work.
26. Context Types
We will initially support:
Company ContextContact ContextOpportunity Context
These are the most useful conversational anchors.
Task and Meeting context can still be retrieved through their direct read tools.
27. Company Context
A Company context package may contain:
Company├── Core Company Data├── Contacts├── Open Opportunities├── Recent Activities├── Open Tasks└── Upcoming Meetings
This answers broad questions about a customer.
28. Contact Context
A Contact context package may contain:
Contact├── Company├── Relevant Opportunities├── Recent Activities├── Open Tasks└── Upcoming Meetings
This answers:
What’s going on with Sarah Johnson?
29. Opportunity Context
An Opportunity context package may contain:
Opportunity├── Company├── Related Contacts├── Recent Activities├── Open Tasks└── Upcoming Meetings
This answers:
What’s happening with the Azure Migration deal?
30. Define the Company Context Contract
Conceptually:
class CompanyCRMContext(BaseModel): company: CompanySummary contacts: list[ContactSummary] opportunities: list[OpportunitySummary] recent_activities: list[ActivitySummary] open_tasks: list[TaskSummary] upcoming_meetings: list[MeetingSummary] metadata: CRMContextMetadata
The context package is structured.
It is not a giant text blob.
31. Why Structured Context?
Structured context lets ChatGPT distinguish:
Company factContact factOpportunity factActivityTaskMeeting
instead of trying to parse an unstructured server-generated paragraph.
This improves grounding.
32. Context Summaries Versus Full Records
Do not return full ORM models.
Instead define purpose-built summaries.
For example:
class OpportunitySummary(BaseModel): id: UUID name: str stage: str amount: Decimal | None probability: int | None expected_close_date: date | None
Only expose what the context needs.
33. Contact Summary
Conceptually:
class ContactSummary(BaseModel): id: UUID full_name: str job_title: str | None company_id: UUID
Avoid returning unnecessary personal data.
34. Activity Summary
Conceptually:
class ActivitySummary(BaseModel): id: UUID activity_type: str occurred_at: datetime subject: str | None summary: str | None
Do not automatically include huge note bodies.
35. Task Summary
Conceptually:
class TaskSummary(BaseModel): id: UUID title: str priority: str due_at: datetime assigned_to_name: str
Only open Tasks belong in the default Company context.
36. Meeting Summary
Conceptually:
class MeetingSummary(BaseModel): id: UUID title: str start_at: datetime end_at: datetime participant_names: list[str]
Only future scheduled Meetings belong in the default context.
37. Context Is Not a Database Dump
This is extremely important.
Avoid:
Company+all Contacts+all Opportunities+all Activities ever+all Tasks ever+all Meetings ever
A large customer could generate thousands of records.
That is not useful context.
38. Context Must Be Bounded
For example:
Contacts:10Open Opportunities:10Recent Activities:20Open Tasks:20Upcoming Meetings:10
These are example limits, not immutable rules.
The key requirement is:
Every collection has a bounded server-controlled maximum.
39. Context Budget
Introduce the concept of a:
Context Budget
The Context Builder must decide how much CRM evidence to return.
For the MVP, this can be record-count based.
Later it may become token-aware.
40. Example Context Budget
DEFAULT_CONTEXT_LIMITS = { "contacts": 10, "opportunities": 10, "activities": 20, "tasks": 20, "meetings": 10,}
The model should not arbitrarily increase these values beyond server limits.
41. Why Context Budgets Matter
Without them:
Large Customer ↓Thousands of Activities ↓Huge Tool Response ↓Large ChatGPT Context ↓Higher Cost ↓Lower Signal
With budgets:
Large Customer ↓Relevant Recent Evidence ↓Bounded Context ↓Better Reasoning
42. Activity Recency Window
For the default Company context, perhaps retrieve:
last 90 days
or:
latest 20 Activities
whichever limit is reached first.
The exact policy should be configurable.
43. Tasks Are State-Oriented
For default context:
status = open
is usually more relevant than completed historical Tasks.
Therefore:
Open Tasks
belong in the context.
Completed Tasks remain available through Task search/history.
44. Meetings Are Future-Oriented
Default Meeting context should retrieve:
status = scheduledANDstart_at >= now
ordered by:
start_at ASC
Historical Meetings are represented through Activities or explicit Meeting history queries.
45. Opportunities Are State-Oriented
Default Company context should usually prioritize:
Open Opportunities
rather than closed historical deals.
Later we can optionally include recent wins/losses.
For the MVP, focus on current work.
46. Contacts Need Prioritization
A Company may have 200 Contacts.
Returning arbitrary Contacts is not useful.
For the MVP, use deterministic ranking such as:
Contacts referenced by open OpportunitiesContacts referenced by recent ActivitiesContacts referenced by open TasksContacts referenced by upcoming MeetingsRecently updated Contacts
This produces more useful context.
47. Key Contacts Are Derived
Do not necessarily store:
is_key_contact
yet.
Instead derive contextual importance from CRM relationships.
Later the domain can support explicit stakeholder roles.
48. Company Context Builder
Introduce:
build_company_context
as an internal application service.
Conceptually:
async def build_company_context( *, company_id: UUID, tenant_context: TenantContext, actor: UserContext,) -> CompanyCRMContext: ...
This should not depend on ChatGPT-specific logic.
49. Context Builder Flow
Load Company ↓Verify Tenant ↓Verify crm.context.read ↓Determine Domain Permissions ↓Load Permitted Contacts ↓Load Permitted Opportunities ↓Load Permitted Recent Activities ↓Load Permitted Open Tasks ↓Load Permitted Upcoming Meetings ↓Apply Limits ↓Rank Results ↓Build Metadata ↓Return Structured Context
50. Context Retrieval Is Read-Only
Part 25 introduces no new business mutation.
Therefore:
get_crm_context
must never:
createupdatedeletecompletecancel
anything.
It is purely retrieval.
51. Context Metadata
Every context package should contain metadata.
Conceptually:
class CRMContextMetadata(BaseModel): generated_at: datetime organization_id: UUID root_entity_type: CRMEntityType root_entity_id: UUID limits: dict[str, int] truncated_sections: list[str] provenance: list["ContextProvenance"]
This makes the package inspectable.
52. Do We Expose Organization ID to ChatGPT?
Internally, metadata may contain it.
But tool response schemas should expose only what is actually useful.
The important point is that tenant scope is recorded and enforced server-side.
Do not make ChatGPT responsible for supplying it.
53. Context Provenance
Every context item should be traceable to its authoritative CRM entity.
For example:
{ "entity_type": "activity", "entity_id": "...", "source": "crm_database"}
This is the beginning of evidence provenance.
54. Why Provenance Matters
Suppose ChatGPT says:
Sarah requested revised pricing.
We should be able to trace that statement to:
ActivityID: ...Occurred: 2 Aug 2026
This becomes essential as AI features become more sophisticated.
55. Context Item References
A useful pattern is to give each context item a stable reference.
For example:
company:cmp_001contact:con_004opportunity:opp_012activity:act_203task:tsk_077meeting:mtg_031
These are conceptual examples.
Internally, UUIDs can remain authoritative.
56. Evidence IDs
We can also generate context-local evidence labels:
E1E2E3E4
Example:
E1 — Azure Migration OpportunityE2 — Customer requested revised pricingE3 — Send revised proposal TaskE4 — Proposal Review Meeting
ChatGPT can use these when reasoning.
57. Do Not Expose Internal Database Complexity
ChatGPT should not need:
SQLAlchemy relationship namesjoin tablesforeign-key structuresrepository implementation details
The Context Layer abstracts those details.
58. Example Company Context Response
Conceptually:
{ "root": { "type": "company", "id": "...", "name": "Adventure Works" }, "contacts": [ { "id": "...", "name": "Sarah Johnson", "job_title": "IT Director" } ], "opportunities": [ { "id": "...", "name": "Azure Migration", "stage": "proposal", "amount": 90000, "currency": "EUR" } ], "recent_activities": [ { "id": "...", "type": "call", "occurred_at": "2026-08-02T...", "summary": "Customer requested revised pricing." } ], "open_tasks": [ { "id": "...", "title": "Send revised proposal", "due_at": "2026-08-07T...", "priority": "high" } ], "upcoming_meetings": [ { "id": "...", "title": "Azure Migration Proposal Review", "start_at": "2026-08-11T...", "participants": [ "Sarah Johnson" ] } ], "metadata": { "generated_at": "...", "truncated_sections": [] }}
This is excellent input for grounded reasoning.
59. Context Ordering
Collections should have deterministic ordering.
For example:
Contacts→ contextual importance DESCOpportunities→ open first→ expected close date ASCActivities→ occurred_at DESCTasks→ due_at ASCMeetings→ start_at ASC
Deterministic context is easier to test.
60. Contact Context Builder
Introduce:
build_contact_context
The root becomes:
Contact
and related evidence includes:
CompanyRelevant OpportunitiesRecent ActivitiesOpen TasksUpcoming Meetings
61. Relevant Opportunities for a Contact
Our current domain may not yet have explicit Opportunity stakeholder relationships.
Therefore only include Opportunities where the relationship can be supported.
Possible evidence:
Contact referenced by OpportunityContact referenced by Opportunity ActivitiesContact referenced by Opportunity TasksContact participating in Opportunity Meetings
Do not fabricate relationships.
62. Opportunity Context Builder
Introduce:
build_opportunity_context
This package contains:
OpportunityCompanyRelevant ContactsRecent ActivitiesOpen TasksUpcoming Meetings
This will become especially important for future Opportunity Intelligence.
63. Opportunity Context Example
User:
What’s happening with the Azure Migration deal?
Quorentra returns:
Azure MigrationCompanyAdventure WorksStageProposalValue€90,000Probability60%Recent ActivityCustomer requested revised pricing.Open TaskSend revised proposal Friday.Upcoming MeetingProposal Review Tuesday.
ChatGPT now has a coherent evidence package.
64. Grounded Answers
We can now establish another important principle:
Every CRM-specific factual claim should be supportable by returned CRM context.
ChatGPT may summarize or interpret.
But it should not invent missing CRM facts.
65. Example Grounded Answer
Evidence:
Opportunity:€90,000Proposal60%Activity:Customer requested revised pricing.Task:Send revised proposal Friday.Meeting:Proposal review Tuesday.
Good answer:
The Azure Migration opportunity is currently in the Proposal stage at €90,000 with a 60% probability. The customer recently requested revised pricing, which is due to be sent Friday, and a proposal review meeting is scheduled for Tuesday.
Every statement is grounded.
66. Unsupported Answer
Bad answer:
Adventure Works is highly likely to sign because Sarah is enthusiastic about the proposal.
Why?
Because:
highly likelyenthusiastic
are not present in the evidence.
Later AI intelligence may infer risk or sentiment.
But those must be clearly identified as analysis, not CRM fact.
67. Facts Versus Analysis
This distinction will become critical.
We should conceptually separate:
CRM Facts
from:
AI Analysis
For now, Part 25 focuses on facts.
Later:
CRM Context ↓AI Analysis
can generate recommendations.
68. Context Completeness
Sometimes context is truncated.
Suppose Adventure Works has:
87 Activities
but we return:
20
Metadata should indicate:
activities_truncated = true
ChatGPT then knows it does not have the entire history.
69. Why Truncation Metadata Matters
Without it, ChatGPT might say:
These are all interactions with Adventure Works.
That would be false.
With metadata, it can say:
Based on the most recent activity…
This is more precise.
70. Context Time Range
Metadata can also expose:
activity_window_startactivity_window_end
when useful.
For example:
Recent Activities:last 90 days
This improves interpretability.
71. Context Freshness
Every package should include:
generated_at
Context is a snapshot.
It should not be treated as permanently current.
72. Do Not Persist Context Packages Initially
For the MVP:
Request ↓Build Context ↓Return Context ↓Discard
Do not create a new persistent Context table yet.
The authoritative data already exists in the domain tables.
73. Why Not Cache Yet?
Caching introduces:
InvalidationStalenessPermission ChangesTenant ChangesRecord Updates
Unless performance requires it, avoid premature caching.
Build correctly first.
74. Query Efficiency
Although context is assembled from multiple domains, avoid classic N+1 query problems.
Use deliberate repository queries.
For example:
1 Company query1 Contacts query1 Opportunities query1 Activities query1 Tasks query1 Meetings query
rather than hundreds of per-record queries.
75. Repository Boundaries
Each domain repository remains responsible for its own data.
For example:
CompanyRepositoryContactRepositoryOpportunityRepositoryActivityRepositoryTaskRepositoryMeetingRepository
The Context Builder orchestrates them.
It does not replace them.
76. Application Service Composition
Conceptually:
CRMContextService│├── CompanyRepository├── ContactRepository├── OpportunityRepository├── ActivityRepository├── TaskRepository└── MeetingRepository
This is a clean application-layer orchestration service.
77. Avoid Cross-Domain ORM Magic
Do not create one enormous ORM relationship graph and serialize everything recursively.
That leads to:
Unbounded loadingCircular relationshipsUnexpected queriesLarge responsesSecurity mistakes
Explicit context queries are safer.
78. Context Policy
Introduce a configuration object:
class CRMContextPolicy: contact_limit = 10 opportunity_limit = 10 activity_limit = 20 task_limit = 20 meeting_limit = 10 activity_lookback_days = 90
Later this can vary by context type.
79. Company Context Policy
For example:
CompanyContacts:10 most relevantOpportunities:10 openActivities:20 most recent within 90 daysTasks:20 openMeetings:10 upcoming
80. Opportunity Context Policy
An Opportunity may need:
Contacts:10 relevantActivities:30 recentTasks:20 openMeetings:10 upcoming
Different context types can have different policies.
81. Context Sections
Each section should indicate whether it was:
includedemptynot_permittedtruncated
This is more informative than simply omitting everything.
82. Example Section Metadata
{ "section": "opportunities", "status": "not_permitted", "returned_count": 0}
This can remain internal if exposing permission details is undesirable.
The important thing is that the Context Builder knows why data is absent.
83. Empty Versus Unauthorized
These are not the same.
No open Opportunities
means something different from:
User cannot read Opportunities
The backend must preserve that distinction.
Whether ChatGPT sees it depends on the response policy.
84. Data Minimization
Only return data necessary for the conversational use case.
For example, Company context may not need:
internal database timestampstechnical audit fieldspassword-related fieldsraw authentication identifiersinternal integration secrets
Obviously those must never enter ChatGPT context.
85. Sensitive Fields
Even within CRM records, some fields may require future field-level policies.
Examples:
personal phone numbersprivate noteslegal informationcommercially restricted data
The MVP can begin with domain-level permissions, but the Context Layer should be designed so field filtering can be added later.
86. Context Sanitization
Introduce a final server-side sanitization step:
Domain Records ↓Permission Filter ↓Field Projection ↓Context Sanitization ↓Context Package
Do not serialize ORM objects directly.
87. Tool Response Stability
Because ChatGPT depends on these schemas, treat context contracts like APIs.
Avoid casually changing:
field namesentity type namessection meaningsdate formatsstatus semantics
Version them deliberately if needed.
88. ChatGPT Tool Surface
Our read tool surface now begins to look like:
search_crmget_crm_contextget_companyget_contactget_opportunityget_activityget_taskget_meeting
The first two handle broad conversational retrieval.
The specific tools handle precise detail.
89. Broad Versus Precise Retrieval
User:
What’s happening with Adventure Works?
Use:
get_crm_context
User:
What is Sarah’s email address?
Use:
get_contact
User:
What is the due date of the revised proposal task?
Use:
get_task
Do not retrieve large context packages when a precise read is sufficient.
90. Context Selection Principle
Use:
smallest authoritative retrievalthat fully answers the question
This reduces latency, cost, and unnecessary data exposure.
91. ChatGPT Context Widget
Create:
chatgpt-ui/src/context/├── CRMContextSummary.tsx├── CompanyContextCard.tsx├── ContactContextCard.tsx└── OpportunityContextCard.tsx
These widgets can provide visual grounding.
92. Company Context Widget
Example:
┌──────────────────────────────────────────────┐│ ADVENTURE WORKS ││ ││ Active Opportunity ││ Azure Migration ││ €90,000 · Proposal · 60% ││ ││ Recent Activity ││ Customer requested revised pricing. ││ ││ Next Task ││ Send revised proposal ││ Friday ││ ││ Next Meeting ││ Proposal Review ││ Tuesday · 14:00 ││ ││ [View Company] [View Opportunity] │└──────────────────────────────────────────────┘
This gives ChatGPT answers a useful visual companion.
93. Opportunity Context Widget
Example:
┌──────────────────────────────────────────────┐│ AZURE MIGRATION ││ Adventure Works ││ ││ €90,000 ││ Proposal ││ 60% Probability ││ ││ Latest Activity ││ Revised pricing requested ││ ││ Open Task ││ Send revised proposal · Friday ││ ││ Upcoming Meeting ││ Proposal Review · Tuesday 14:00 │└──────────────────────────────────────────────┘
94. Do Not Let Widgets Become Authoritative
Widgets render server-returned data.
They do not calculate:
Opportunity stageTask statusMeeting timeAmountsProbabilities
The backend remains authoritative.
95. Context and Conversation
Suppose the user asks:
What’s happening with Adventure Works?
ChatGPT retrieves the context.
Then the user asks:
When is the meeting?
The conversation may already contain the Meeting context.
But for authoritative answers, we should be careful about stale state.
96. Freshness Strategy
For the MVP:
same immediate conversational turn→ context can be reusedlater authoritative request→ reread if freshness matters
Especially before any mutation, always reread current state.
97. Context Is Never Mutation Authority
Suppose context says:
Task version = 1
Then the user says:
Complete it.
Do not execute directly from cached conversational context.
The mutation preparation step reloads the Task and verifies current state.
This preserves our architecture.
98. Read Path Versus Write Path
We now have a strong separation:
READ PATHUser ↓ChatGPT ↓Search / Context ↓Authorized CRM Data ↓Grounded Answer
and:
WRITE PATHUser ↓ChatGPT ↓Draft ↓Validation ↓Preparation ↓Confirmation ↓Execution ↓Audit
This is one of the most important architectural patterns in Quorentra.
99. Context Error Model
Introduce structured errors such as:
CRM_ENTITY_NOT_FOUNDCRM_ENTITY_AMBIGUOUSCRM_CONTEXT_FORBIDDENCRM_CONTEXT_TYPE_UNSUPPORTEDCRM_CONTEXT_BUILD_FAILED
ChatGPT can respond appropriately.
100. Entity Not Found
User:
What’s happening with Northstar Dynamics?
No match.
Return:
{ "code": "CRM_ENTITY_NOT_FOUND", "query": "Northstar Dynamics"}
Do not invent a customer.
101. Ambiguous Entity
Return candidates:
{ "code": "CRM_ENTITY_AMBIGUOUS", "candidates": [ { "type": "company", "id": "...", "name": "Northstar Dynamics Europe" }, { "type": "company", "id": "...", "name": "Northstar Dynamics US" } ]}
ChatGPT asks for clarification.
102. Unsupported Context Root
If someone requests full CRM context around:
Activity
but the MVP only supports:
CompanyContactOpportunity
return:
CRM_CONTEXT_TYPE_UNSUPPORTED
Do not silently invent semantics.
103. Search Audit
Searches generally do not need business AuditEvents like mutations do.
But operational logs may record:
search typetenantactorresult countduration
without storing unnecessary query content.
104. Context Access Logging
Because context retrieval may expose multiple CRM records, consider structured access logging.
For the MVP:
context_typeroot_entity_idorganization_idactor_user_idsections_returnedduration_ms
This is useful for security and diagnostics.
105. Metrics
Useful metrics include:
crm_search_requests_totalcrm_search_zero_results_totalcrm_search_ambiguous_totalcrm_context_requests_totalcrm_context_failures_totalcrm_context_truncated_totalcrm_context_build_duration_ms
Later we can measure which context sections are most useful.
106. Search Performance
Indexes should support common search fields.
For example:
Company.nameContact.first_nameContact.last_nameContact.emailOpportunity.nameTask.titleMeeting.title
Tenant ID should be part of the relevant index strategy.
107. Example Company Index
Conceptually:
CREATE INDEX idx_company_org_nameON companies (organization_id, name);
Equivalent indexes can support other domain searches.
108. PostgreSQL Text Search
If simple ILIKE becomes insufficient, PostgreSQL gives us several options before vector search:
pg_trgmGIN indexesFull-text search
These are excellent tools for structured CRM search.
109. pg_trgm
For fuzzy name matching:
Adventure Wroks
can still potentially match:
Adventure Works
using trigram similarity.
This may be a useful incremental improvement.
110. Keep Search Explainable
For CRM entity resolution, deterministic search is valuable.
We want to understand why:
Adventure Works
matched a particular Company.
This is easier with explicit matching rules than opaque semantic similarity.
111. Context Builder Tests
Test:
Company contextContact contextOpportunity contexttenant isolationpermission filteringlimitsorderingtruncationempty sections
112. Unified Search Tests
Verify:
exact Company matchprefix Company matchContact matchOpportunity matchTask matchMeeting matchmultiple entity typesentity type filterresult limit
113. Tenant Isolation Search Test
Tenant A:
Adventure Works
Tenant B:
Adventure Works
User in Tenant A searches.
Expected:
Tenant A result only
114. Permission-Aware Search Test
User lacks:
opportunities.read
Search:
Azure
Expected:
Opportunity results omitted
even if matching Opportunities exist.
115. Company Context Permission Test
User has:
companies.readcontacts.readtasks.read
but lacks:
opportunities.readactivities.readmeetings.read
Context must contain only permitted sections.
116. Company Context Limit Test
Create:
100 Contacts50 Opportunities500 Activities80 Tasks30 Meetings
Context must respect configured limits.
117. Activity Ordering Test
Activities returned:
newest ↓oldest
within the context window.
118. Task Ordering Test
Open Tasks returned:
earliest due ↓latest due
119. Meeting Ordering Test
Upcoming Meetings returned:
nearest ↓furthest
120. Opportunity Filtering Test
Default Company context should include:
open Opportunities
and exclude closed historical Opportunities unless policy says otherwise.
121. Activity Window Test
If policy is:
90 days
an Activity from:
120 days ago
should not appear in default context.
122. Truncation Test
If there are:
40 Activities
and limit is:
20
return:
20 Activitiesmetadata:activities_truncated = true
123. Empty Context Section Test
Company has no Meetings.
Return:
upcoming_meetings = []
not an invented placeholder Meeting.
124. Context Provenance Test
Every returned item must retain enough information to identify:
entity typeentity IDsource
125. No Mutation Test
Call:
get_crm_context
Verify:
0 database business mutations0 MutationRequests0 AuditEvents
unless access auditing is separately implemented.
126. Query Count Test
Prevent accidental N+1 behavior.
For a Company context containing ten Contacts, the system should not perform dozens of repeated queries unnecessarily.
Set performance expectations.
127. Context Serialization Test
Verify ORM-only or sensitive fields never appear in the tool response.
For example:
password hashesinternal secretsraw security metadata
must never be serialized.
128. Grounding Tests
Given known context, ask ChatGPT:
What’s happening with Adventure Works?
Verify the answer does not claim unsupported:
sentimentcommitmentriskcustomer intentionfuture actions
unless present in evidence.
129. Missing Evidence Test
No Task exists.
ChatGPT must not say:
You need to send the proposal Friday.
unless that obligation is present elsewhere in authoritative context.
130. Truncated Context Language
If Activity history is truncated, responses should prefer language such as:
Based on the recent activity...
rather than:
The complete history shows...
This is an important grounding behavior.
131. Context Refresh Test
Retrieve context.
Modify Opportunity.
Retrieve context again.
Expected:
new authoritative state
because context is built dynamically.
132. Context Before Mutation
Retrieve context showing:
Task openversion 1
Change Task elsewhere.
Then attempt completion.
Mutation preparation must reload the Task rather than trust the earlier context.
133. Context Security Test
Attempt to request:
entity_id
belonging to another tenant.
Expected:
not found / forbidden according to security policy
Never return cross-tenant context.
134. Logging
Useful structured fields:
context_request_idroot_entity_typeroot_entity_idorganization_idactor_user_idsections_requestedsections_returnedresult_countstruncated_sectionsduration_ms
Avoid logging full CRM context payloads.
135. Context Request ID
Introduce:
context_request_id
for operational traceability.
This is not a persistent business entity.
It simply helps correlate:
Tool requestContext BuilderRepository callsResponse
136. Future AI Traceability
Later, an AI recommendation may be associated with:
context_request_id
or a richer evidence snapshot identifier.
This helps answer:
What evidence produced this recommendation?
We are preparing for that future now.
137. Version Update
Part 25 introduces:
Unified CRM SearchCRM Context LayerCompany ContextContact ContextOpportunity ContextContext BudgetsPermission-Aware RetrievalContext ProvenanceGrounded CRM Answers
Update:
app/core/constants.py
from:
APP_VERSION = "0.11.0"
to:
APP_VERSION = "0.12.0"
138. Quorentra 0.12.0
Our modular MVP now looks like:
Platform├── FastAPI ✓├── PostgreSQL ✓├── SQLAlchemy ✓└── Alembic ✓Identity├── Organizations ✓├── Users ✓├── Memberships ✓├── Authentication ✓└── JWT ✓Security├── TenantContext ✓├── Tenant Isolation ✓├── RBAC ✓├── Domain Permissions ✓└── Context-Aware Authorization ✓CRM├── Companies ✓├── Contacts ✓├── Opportunities ✓├── Activities ✓├── Tasks ✓└── Meetings ✓Sales├── Pipeline ✓├── Opportunity Stages ✓├── Probability ✓└── Weighted Pipeline ✓Work Management├── Tasks ✓├── Due Dates ✓├── Priorities ✓├── Assignments ✓└── Completion ✓Scheduling├── Meetings ✓├── Participants ✓├── Conflict Detection ✓├── Cancellation ✓└── Completion ✓CRM Memory├── Activity Timeline ✓├── Calls ✓├── Emails ✓├── Meeting Activities ✓└── Notes ✓Interfaces├── REST ✓├── MCP ✓└── ChatGPT ✓Mutation Governance├── Drafts ✓├── Mutation Requests ✓├── Confirmation ✓├── Expiry ✓├── Cancellation ✓├── Idempotency ✓├── Transactions ✓├── Optimistic Concurrency ✓└── Audit Events ✓Conversational Operations├── Entity Resolution ✓├── Member Resolution ✓├── Participant Resolution ✓├── Relative Time Resolution ✓├── Relationship Validation ✓├── Missing-Field Detection ✓├── Field Provenance ✓├── Multi-Turn Completion ✓└── Safe Execution ✓CRM Retrieval├── Unified CRM Search ✓├── Tenant-Scoped Search ✓├── Permission-Aware Search ✓├── Search Ranking ✓└── Bounded Search Results ✓CRM Context Layer├── Company Context ✓├── Contact Context ✓├── Opportunity Context ✓├── Permission Filtering ✓├── Tenant Filtering ✓├── Context Budgets ✓├── Recency Windows ✓├── Result Ranking ✓├── Truncation Metadata ✓├── Context Provenance ✓├── Data Minimization ✓└── Structured Context Packages ✓ChatGPT UI├── Pipeline ✓├── CRM Entity Detail ✓├── Activity Timeline ✓├── Task Views ✓├── Meeting Views ✓├── Mutation Confirmation ✓└── CRM Context Widgets ✓ChatGPT CRM Operations├── Search CRM ✓├── Retrieve CRM Context ✓├── Read Companies ✓├── Read Contacts ✓├── Read Opportunities ✓├── Read Activities ✓├── Read Tasks ✓├── Read Meetings ✓├── Create Company ✓├── Create Contact ✓├── Create Opportunity ✓├── Update Opportunity Stage ✓├── Record Activity ✓├── Create Task ✓├── Complete Task ✓├── Schedule Meeting ✓├── Cancel Meeting ✓└── Complete Meeting ✓Grounding├── Structured Evidence ✓├── Evidence Provenance ✓├── Context Freshness ✓├── Context Completeness Metadata ✓└── Fact/Analysis Separation ✓Semantic Knowledge Retrieval -AI Intelligence -
139. Acceptance Criteria
Part 25 is complete when:
✓ Part 24 regression suite remains green✓ crm.search exists✓ crm.context.read exists✓ domain permissions remain authoritative✓ context retrieval cannot bypass domain permissions✓ search_crm exists✓ search supports Company✓ search supports Contact✓ search supports Opportunity✓ search supports Activity where appropriate✓ search supports Task✓ search supports Meeting✓ search is tenant-scoped✓ search is permission-aware✓ search results are bounded✓ search results are ranked deterministically✓ search returns entity type✓ search returns authoritative entity ID✓ exact matches rank appropriately✓ ambiguous matches are not silently resolved✓ zero-result searches do not invent entities✓ CRMSearchResult contract exists✓ search schemas expose no unnecessary internal data✓ get_crm_context exists✓ context operates on resolved entity IDs✓ Company context exists✓ Contact context exists✓ Opportunity context exists✓ unsupported context roots return structured errors✓ Company context can include permitted Contacts✓ Company context can include permitted Opportunities✓ Company context can include permitted Activities✓ Company context can include permitted Tasks✓ Company context can include permitted Meetings✓ Contact context includes Company where permitted✓ Contact context includes only supported related Opportunities✓ Contact context includes recent Activities✓ Contact context includes open Tasks✓ Contact context includes upcoming Meetings✓ Opportunity context includes Company✓ Opportunity context includes supported Contacts✓ Opportunity context includes recent Activities✓ Opportunity context includes open Tasks✓ Opportunity context includes upcoming Meetings✓ every context query is tenant-scoped✓ cross-tenant IDs never return context✓ every context section respects domain permissions✓ unauthorized sections do not leak data✓ purpose-built summary schemas exist✓ ORM models are not serialized directly✓ unnecessary fields are excluded✓ sensitive technical fields never enter context✓ context budgets exist✓ Contacts are bounded✓ Opportunities are bounded✓ Activities are bounded✓ Tasks are bounded✓ Meetings are bounded✓ default Activity recency window exists✓ default Company context prioritizes open Opportunities✓ default Task context returns open Tasks✓ default Meeting context returns future scheduled Meetings✓ Contact ranking is deterministic✓ Activity ordering is deterministic✓ Task ordering is deterministic✓ Meeting ordering is deterministic✓ truncation is detected✓ truncation metadata exists✓ generated_at exists✓ context freshness is explicit✓ empty sections remain empty rather than invented✓ context provenance exists✓ context items retain authoritative entity identity✓ evidence can be traced to CRM records✓ context packages are structured✓ context is not returned as one unstructured text blob✓ ChatGPT receives enough type information to distinguish evidence✓ CRMContextService exists✓ Context Builder orchestrates domain repositories✓ domain repositories remain independent✓ Context Builder avoids recursive ORM serialization✓ Context Builder avoids obvious N+1 behavior✓ context retrieval is read-only✓ get_crm_context creates no business mutation✓ get_crm_context creates no MutationRequest✓ broad questions can use CRM Context✓ precise questions can use specific read tools✓ smallest sufficient authoritative retrieval is preferred✓ Company context widget exists✓ Contact context widget exists✓ Opportunity context widget exists✓ widgets render authoritative server data✓ context snapshots are not trusted for later mutations✓ mutation preparation reloads current authoritative state✓ context does not bypass optimistic concurrency✓ CRM_ENTITY_NOT_FOUND exists✓ CRM_ENTITY_AMBIGUOUS exists✓ CRM_CONTEXT_FORBIDDEN exists✓ CRM_CONTEXT_TYPE_UNSUPPORTED exists✓ CRM_CONTEXT_BUILD_FAILED exists✓ context access logging avoids full payload logging✓ context metrics exist✓ context request correlation exists✓ grounded answers use returned CRM evidence✓ unsupported CRM facts are not invented✓ facts and AI analysis remain conceptually separate✓ truncated context does not produce claims of completeness✓ Quorentra reports version 0.12.0
Most importantly:
ChatGPT can now ask Quorentra for a bounded, permission-aware, tenant-isolated package of authoritative customer evidence instead of manually reconstructing customer state through unrelated low-level queries.
140. What We Have Achieved
The question:
What’s happening with Adventure Works?
can now flow through:
Natural Language ↓Search CRM ↓Resolve Adventure Works ↓Company ↓CRM Context Builder ↓┌─────────────────────────────┐│ Company ││ Contacts ││ Opportunities ││ Recent Activities ││ Open Tasks ││ Upcoming Meetings │└─────────────────────────────┘ ↓Permission Filtering ↓Tenant Filtering ↓Context Budget ↓Provenance ↓Structured Evidence ↓ChatGPT ↓Grounded Answer
This is a major architectural milestone.
141. Quorentra Now Has an Evidence Layer
Our architecture has evolved from:
ChatGPT ↓CRM Tools ↓CRM Domains ↓PostgreSQL
to:
ChatGPT
│
┌──────────┴──────────┐
│ │
▼ ▼
CRM Context CRM Mutations
Layer │
│ │
▼ ▼
CRM Read Model Governed Write Path
│ │
└──────────┬──────────┘
▼
CRM Domains
│
▼
PostgreSQL
The read and write paths now have different responsibilities.
142. The Read Path
The read path is optimized for:
RetrievalContextGroundingExplanation
It looks like:
User Question ↓ChatGPT ↓Entity Resolution ↓Context Builder ↓Authorized Evidence ↓Grounded Answer
143. The Write Path
The write path remains optimized for:
SafetyValidationConfirmationConsistencyAuditability
It looks like:
User Intent ↓ChatGPT ↓Draft ↓Validation ↓Preparation ↓Confirmation ↓Mutation ↓Audit
This separation is deliberate.
144. Why This Is Important for ChatGPT-Native CRM
Traditional CRM systems usually optimize around:
ScreensFormsTablesNavigation
Quorentra must also optimize around:
QuestionsContextIntentEvidenceActions
That requires a different application architecture.
The Context Layer is part of that difference.
145. We Can Now Ask More Natural Questions
For example:
What’s happening with Adventure Works?
What was my last interaction with Sarah?
What do I need to do next for the Azure Migration deal?
Do we have anything scheduled with Contoso?
Which open opportunities does Adventure Works have?
What customer work do I have today?
These questions can increasingly be answered through structured Quorentra evidence.
146. But We Still Have Only Structured CRM Memory
The Context Layer retrieves:
CompaniesContactsOpportunitiesActivitiesTasksMeetings
But real CRM knowledge also exists inside:
Proposal documentsContractsMeeting notesEmailsTechnical documentsCustomer requirementsStatements of workPresentationsPDFsDOCX files
Those cannot always be represented as ordinary relational fields.
147. The Next Architectural Layer
To make ChatGPT genuinely knowledgeable about customer relationships, Quorentra eventually needs:
Structured CRM Context +Unstructured Customer Knowledge
That means introducing a document and knowledge architecture.
But we should continue following the same modular principle.
Do not jump directly to:
RAG
First establish the authoritative document domain.
148. Documents Before Embeddings
Before asking:
How should we chunk documents?
we first need to answer:
What is a Document?Who owns it?Which tenant does it belong to?Which Company does it belong to?Which Opportunity does it belong to?Who uploaded it?What is its MIME type?Where is the file stored?What is its processing state?Can the user access it?How do we audit it?
Only after those foundations exist should we introduce extraction and embeddings.
149. Why This Fits the Modular Strategy
We continue adding one coherent capability at a time.
So far:
Identity ↓Security ↓Companies ↓Contacts ↓Opportunities ↓Activities ↓Tasks ↓Meetings ↓CRM Context
Next:
Documents
Then later:
Text Extraction ↓Chunking ↓Embeddings ↓Vector Search ↓RAG
Each stage produces a working system.
150. Next Article
Adding CRM Documents and Attachments — Uploads, Storage, Metadata, Entity Relationships, Permissions, and Secure Retrieval
We will introduce:
documents.readdocuments.createdocuments.deleteDocumentDocument StatusDocument TypeOriginal FilenameMIME TypeFile SizeStorage KeyChecksumUploaded ByCompany RelationshipContact RelationshipOpportunity RelationshipActivity RelationshipTask RelationshipMeeting RelationshipTenant IsolationSecure UploadFile ValidationFile Size LimitsMIME ValidationExtension ValidationStorage AbstractionLocal Development StorageFuture Object Storage CompatibilityDocument MetadataDocument SearchDocument ListingDocument DownloadSigned / Authorized RetrievalDocument DeletionAudit EventsChatGPT Document ToolsDocument Widgets
Our target interaction will eventually become:
User:"Attach the revised Azure migration proposalto the Adventure Works opportunity."
Quorentra will resolve:
Adventure Works ↓CompanyAzure Migration ↓Opportunity
and associate the uploaded file with the correct CRM context.
Later, once text extraction and RAG are added, the user will be able to ask:
What pricing did we propose to Adventure Works?
Quorentra will combine:
Structured CRM Context +Relevant Document Knowledge ↓Grounded ChatGPT Answer
That will begin the next major phase of Quorentra — A Modular, ChatGPT-Native AI CRM: moving from structured CRM records toward a customer knowledge platform.