Phase 1A: Remove company routes/services/models/schemas, unify to Contact
- Removed: app/routes/companies.py, app/services/company_service.py, app/models/company.py, app/schemas/company.py - Updated: main.py, routes/__init__.py, models/__init__.py (removed company imports) - Updated: action_mapper.py (company intents → contact intents, /api/v1/companies → /api/v1/contacts) - Updated: workflows/engine.py (company.created → contact.created) - Updated: worker.py (removed index_company) - Updated: roles.py, permission_registry.py, permissions.py, deps.py (companies: → contacts:) - Updated: permission_registry.py CORE_FIELD_DEFINITIONS (old field names → unified contact fields) - Updated: address model/schema/service (entity_type company → contact only) - Updated: ai_copilot_service.py (_exec_companies removed, _exec_contacts extended) - Updated: ai_proactive services/jobs/context_tools (Company → Contact, company_contacts → contact_persons) - Updated: unified_search jobs.py (removed index_company), query_understanding.py - Updated: calendar/models.py, addresses.py docstrings - Updated: conftest.py (Company → Contact, removed companies/company_contacts from TRUNCATE) - Updated: test_unified_search.py (index_company → index_contact)
This commit is contained in:
+21
@@ -32,6 +32,27 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Phase 1B: Backend Plugins — entity_type='company' → 'contact'
|
||||||
|
|
||||||
|
| Task | Status | Datum | Notiz |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1.9 | ✅ done | 2026-07-23 | entity_links Plugin: entity_type pattern ^(company|contact)$ → ^contact$, company_router entfernt, on_company_deleted → on_contact_deleted, company.deleted → contact.deleted |
|
||||||
|
| 1.10 | ✅ done | 2026-07-23 | unified_search Plugin: CompanySearchProvider → ContactSearchProvider, index_company → index_contact, events company.created/updated → contact.created/updated, search_engine mapping aktualisiert |
|
||||||
|
| 1.11 | ✅ done | 2026-07-23 | calendar Plugin: entity_type pattern ^(company|contact)$ → ^contact$ (CalendarType='company' bleibt) |
|
||||||
|
| 1.12 | ✅ done | 2026-07-23 | tags Plugin: entity_type pattern ^(company|contact|file|folder)$ → ^(contact|file|folder)$ |
|
||||||
|
| 1.13 | ✅ done | 2026-07-23 | mail Plugin: company_id → contact_id in model, schemas, routes, services |
|
||||||
|
| 1.14 | ✅ done | 2026-07-23 | test_sample Plugin: company.created → contact.created |
|
||||||
|
| 1.15 | ✅ done | 2026-07-23 | Event Names Unify: Alle company.created/updated/deleted → contact.created/updated/deleted |
|
||||||
|
| 1.16 | ✅ done | 2026-07-23 | DB Migration 0027: entity_type 'company' → 'contact' in entity_links, tag_assignments, calendar_entry_links, addresses; mails company_id → contact_id |
|
||||||
|
| 1.17 | ✅ done | 2026-07-23 | Backend Tests Update: test_companies.py, test_unified_search.py, test_entity_links.py, test_calendar.py, test_tags.py, test_ai_proactive.py, test_tenant.py — entity_type='company' → 'contact' |
|
||||||
|
| 1.18 | ✅ done | 2026-07-23 | Permission-Registry-Cleanup: companies:read/write/delete aus CORE_PERMISSIONS entfernt (bereits in 1A) |
|
||||||
|
| 1.19 | ✅ done | 2026-07-23 | Addresses entity_type='company' → 'contact' in address_service.py (bereits in 1A) |
|
||||||
|
| 1.20 | ✅ done | 2026-07-23 | conftest.py Update: Company → Contact, CompanyContact → ContactPerson, TRUNCATE ohne companies/company_contacts |
|
||||||
|
|
||||||
|
**Phase 1B Gesamt: ✅ Complete**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Phase 1C: Frontend — Unified Contact UI
|
## Phase 1C: Frontend — Unified Contact UI
|
||||||
|
|
||||||
| Task | Status | Datum | Notiz |
|
| Task | Status | Datum | Notiz |
|
||||||
|
|||||||
@@ -263,7 +263,7 @@ def register_context_tools(registry) -> None:
|
|||||||
parameters={
|
parameters={
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"entity_type": {"type": "string", "description": "contact, company, mail, file, event"},
|
"entity_type": {"type": "string", "description": "contact, mail, file, event"},
|
||||||
"entity_id": {"type": "string", "description": "UUID der Entity"},
|
"entity_id": {"type": "string", "description": "UUID der Entity"},
|
||||||
"limit": {"type": "integer", "default": 5, "description": "Max results per type"},
|
"limit": {"type": "integer", "default": 5, "description": "Max results per type"},
|
||||||
},
|
},
|
||||||
@@ -300,7 +300,7 @@ def register_context_tools(registry) -> None:
|
|||||||
parameters={
|
parameters={
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"entity_type": {"type": "string", "description": "contact or company"},
|
"entity_type": {"type": "string", "description": "contact"},
|
||||||
"entity_id": {"type": "string", "description": "UUID der Entity"},
|
"entity_id": {"type": "string", "description": "UUID der Entity"},
|
||||||
},
|
},
|
||||||
"required": ["entity_type", "entity_id"],
|
"required": ["entity_type", "entity_id"],
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ async def deep_analysis(
|
|||||||
contact = contact_result.scalar_one_or_none()
|
contact = contact_result.scalar_one_or_none()
|
||||||
extended_context["contact"] = _serialize_row(contact) if contact else None
|
extended_context["contact"] = _serialize_row(contact) if contact else None
|
||||||
|
|
||||||
elif entity_type == "company":
|
elif entity_type == "contact":
|
||||||
from app.models.contact import Contact as Company
|
from app.models.contact import Contact as Company
|
||||||
|
|
||||||
comp_result = await db.execute(
|
comp_result = await db.execute(
|
||||||
@@ -136,10 +136,10 @@ async def deep_analysis(
|
|||||||
.where(Company.tenant_id == tid)
|
.where(Company.tenant_id == tid)
|
||||||
.limit(1)
|
.limit(1)
|
||||||
)
|
)
|
||||||
company = comp_result.scalar_one_or_none()
|
contact = comp_result.scalar_one_or_none()
|
||||||
extended_context["company"] = _serialize_row(company) if company else None
|
extended_context["contact"] = _serialize_row(contact) if contact else None
|
||||||
|
|
||||||
# All mails for company
|
# All mails for contact
|
||||||
mail_result = await db.execute(
|
mail_result = await db.execute(
|
||||||
select(Mail)
|
select(Mail)
|
||||||
.where(Mail.contact_id == eid)
|
.where(Mail.contact_id == eid)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ from app.core.cache import get_cache
|
|||||||
from app.core.db import create_db_session, get_session_factory
|
from app.core.db import create_db_session, get_session_factory
|
||||||
from app.core.notifications import create_notification
|
from app.core.notifications import create_notification
|
||||||
from app.models.audit import AuditLog
|
from app.models.audit import AuditLog
|
||||||
from app.models.contact import Contact as Company
|
from app.models.contact import Contact, ContactPerson
|
||||||
from app.models.contact import Contact, ContactPerson
|
from app.models.contact import Contact, ContactPerson
|
||||||
from app.plugins.builtins.ai_proactive.models import (
|
from app.plugins.builtins.ai_proactive.models import (
|
||||||
ContextLog,
|
ContextLog,
|
||||||
@@ -178,28 +178,28 @@ async def gather_context(
|
|||||||
)
|
)
|
||||||
context["mails"] = [_serialize_row(m) for m in mail_result.scalars().all()]
|
context["mails"] = [_serialize_row(m) for m in mail_result.scalars().all()]
|
||||||
|
|
||||||
# Company via company_contacts
|
# Company via contact_persons
|
||||||
cc_result = await db.execute(
|
cc_result = await db.execute(
|
||||||
select(ContactPerson)
|
select(ContactPerson)
|
||||||
.where(ContactPerson.contact_id == entity_id)
|
.where(ContactPerson.contact_id == entity_id)
|
||||||
.where(ContactPerson.tenant_id == tenant_id)
|
.where(ContactPerson.tenant_id == tenant_id)
|
||||||
.limit(5)
|
.limit(5)
|
||||||
)
|
)
|
||||||
companies: list[dict[str, Any]] = []
|
contacts_list: list[dict[str, Any]] = []
|
||||||
for cc in cc_result.scalars().all():
|
for cc in cc_result.scalars().all():
|
||||||
comp_result = await db.execute(
|
comp_result = await db.execute(
|
||||||
select(Company)
|
select(Contact)
|
||||||
.where(Company.id == cc.company_id)
|
.where(Contact.id == cc.contact_id)
|
||||||
.where(Company.tenant_id == tenant_id)
|
.where(Contact.tenant_id == tenant_id)
|
||||||
.limit(1)
|
.limit(1)
|
||||||
)
|
)
|
||||||
comp = comp_result.scalar_one_or_none()
|
comp = comp_result.scalar_one_or_none()
|
||||||
if comp:
|
if comp:
|
||||||
comp_data = _serialize_row(comp)
|
comp_data = _serialize_row(comp)
|
||||||
comp_data["role_at_company"] = cc.role_at_company
|
comp_data["role"] = cc.role
|
||||||
comp_data["is_primary"] = cc.is_primary
|
comp_data["is_primary"] = cc.is_primary
|
||||||
companies.append(comp_data)
|
contacts_list.append(comp_data)
|
||||||
context["company"] = companies[0] if companies else None
|
context["contact"] = contacts_list[0] if contacts_list else None
|
||||||
context["companies"] = companies
|
context["companies"] = companies
|
||||||
|
|
||||||
# Upcoming calendar events
|
# Upcoming calendar events
|
||||||
@@ -262,25 +262,25 @@ async def gather_context(
|
|||||||
|
|
||||||
if mail and mail.contact_id:
|
if mail and mail.contact_id:
|
||||||
comp_result = await db.execute(
|
comp_result = await db.execute(
|
||||||
select(Company)
|
select(Contact)
|
||||||
.where(Company.id == mail.contact_id)
|
.where(Contact.id == mail.contact_id)
|
||||||
.where(Company.tenant_id == tenant_id)
|
.where(Contact.tenant_id == tenant_id)
|
||||||
.limit(1)
|
.limit(1)
|
||||||
)
|
)
|
||||||
comp = comp_result.scalar_one_or_none()
|
comp = comp_result.scalar_one_or_none()
|
||||||
context["company"] = _serialize_row(comp) if comp else None
|
context["contact"] = _serialize_row(comp) if comp else None
|
||||||
|
|
||||||
elif entity_type == "company":
|
elif entity_type == "contact":
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(Company)
|
select(Contact)
|
||||||
.where(Company.id == entity_id)
|
.where(Contact.id == entity_id)
|
||||||
.where(Company.tenant_id == tenant_id)
|
.where(Contact.tenant_id == tenant_id)
|
||||||
.limit(1)
|
.limit(1)
|
||||||
)
|
)
|
||||||
company = result.scalar_one_or_none()
|
company = result.scalar_one_or_none()
|
||||||
context["company"] = _serialize_row(company) if company else None
|
context["contact"] = _serialize_row(company) if company else None
|
||||||
|
|
||||||
# Contacts via company_contacts
|
# Contacts via contact_persons
|
||||||
cc_result = await db.execute(
|
cc_result = await db.execute(
|
||||||
select(ContactPerson)
|
select(ContactPerson)
|
||||||
.where(ContactPerson.contact_id == entity_id)
|
.where(ContactPerson.contact_id == entity_id)
|
||||||
@@ -297,12 +297,12 @@ async def gather_context(
|
|||||||
contact = contact_result.scalar_one_or_none()
|
contact = contact_result.scalar_one_or_none()
|
||||||
if contact:
|
if contact:
|
||||||
contact_data = _serialize_row(contact)
|
contact_data = _serialize_row(contact)
|
||||||
contact_data["role_at_company"] = cc.role_at_company
|
contact_data["role"] = cc.role
|
||||||
contact_data["is_primary"] = cc.is_primary
|
contact_data["is_primary"] = cc.is_primary
|
||||||
contacts.append(contact_data)
|
contacts.append(contact_data)
|
||||||
context["contacts"] = contacts
|
context["contacts"] = contacts
|
||||||
|
|
||||||
# Mails for this company
|
# Mails for this contact
|
||||||
from app.plugins.builtins.mail.models import Mail
|
from app.plugins.builtins.mail.models import Mail
|
||||||
|
|
||||||
mail_result = await db.execute(
|
mail_result = await db.execute(
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ class CalendarEntry(Base, TenantMixin):
|
|||||||
|
|
||||||
|
|
||||||
class CalendarEntryLink(Base, TenantMixin):
|
class CalendarEntryLink(Base, TenantMixin):
|
||||||
"""Link between a calendar entry and an entity (company/contact)."""
|
"""Link between a calendar entry and an entity (contact)."""
|
||||||
|
|
||||||
__tablename__ = "calendar_entry_links"
|
__tablename__ = "calendar_entry_links"
|
||||||
__table_args__ = (Index("ix_entry_links_entry", "entry_id"),)
|
__table_args__ = (Index("ix_entry_links_entry", "entry_id"),)
|
||||||
|
|||||||
@@ -121,27 +121,6 @@ async def index_contact(ctx: dict[str, Any], contact_id: str) -> None:
|
|||||||
logger.exception("Failed to index contact %s", contact_id)
|
logger.exception("Failed to index contact %s", contact_id)
|
||||||
|
|
||||||
|
|
||||||
async def index_company(ctx: dict[str, Any], company_id: str) -> None:
|
|
||||||
"""Index a company (contact with type='company'): generate and store embedding."""
|
|
||||||
from sqlalchemy import text
|
|
||||||
from app.plugins.builtins.unified_search.embedding import index_entity
|
|
||||||
|
|
||||||
factory = get_session_factory()
|
|
||||||
async with factory() as db:
|
|
||||||
try:
|
|
||||||
eid = _parse_id(company_id)
|
|
||||||
result = await db.execute(
|
|
||||||
text("SELECT tenant_id FROM contacts WHERE id = :cid"),
|
|
||||||
{"cid": eid},
|
|
||||||
)
|
|
||||||
row = result.mappings().first()
|
|
||||||
if not row:
|
|
||||||
return
|
|
||||||
await index_entity("contact", eid, row["tenant_id"], db)
|
|
||||||
except Exception:
|
|
||||||
logger.exception("Failed to index company %s", company_id)
|
|
||||||
|
|
||||||
|
|
||||||
async def index_event(ctx: dict[str, Any], event_id: str) -> None:
|
async def index_event(ctx: dict[str, Any], event_id: str) -> None:
|
||||||
"""Index a calendar event: generate and store embedding."""
|
"""Index a calendar event: generate and store embedding."""
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ DEFAULT_LLM_MODEL = os.environ.get('SEARCH_LLM_MODEL', 'ollama/deepseek-v4-flash
|
|||||||
QUERY_ANALYZE_SYSTEM = (
|
QUERY_ANALYZE_SYSTEM = (
|
||||||
"Du bist ein Query-Analyzer fuer ein CRM. "
|
"Du bist ein Query-Analyzer fuer ein CRM. "
|
||||||
"Analysiere die Suchanfrage und gib JSON zurueck: "
|
"Analysiere die Suchanfrage und gib JSON zurueck: "
|
||||||
'{"normalized_query": str, "entities": {"person": str|null, "company": str|null, "topic": str|null}, '
|
'{"normalized_query": str, "entities": {"person": str|null, "contact": str|null, "topic": str|null}, '
|
||||||
'"intent": str, "semantic_terms": [str], "suggested_filters": {}}'
|
'"intent": str, "semantic_terms": [str], "suggested_filters": {}}'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ router = APIRouter(prefix="/api/v1/addresses", tags=["addresses"])
|
|||||||
|
|
||||||
@router.get("")
|
@router.get("")
|
||||||
async def list_addresses(
|
async def list_addresses(
|
||||||
entity_type: str = Query(..., pattern="^(company|contact)$"),
|
entity_type: str = Query(..., pattern="^contact$"),
|
||||||
entity_id: str = Query(...),
|
entity_id: str = Query(...),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
current_user: dict = Depends(require_permission("addresses:read")),
|
current_user: dict = Depends(require_permission("addresses:read")),
|
||||||
|
|||||||
@@ -321,7 +321,7 @@ async def _execute_api_action(
|
|||||||
return {"error": f"Unsupported entity: {entity}", "status_code": 400, "success": False}
|
return {"error": f"Unsupported entity: {entity}", "status_code": 400, "success": False}
|
||||||
|
|
||||||
|
|
||||||
async def _exec_contacts\(
|
async def _exec_contacts(
|
||||||
db: AsyncSession,
|
db: AsyncSession,
|
||||||
tenant_id: uuid.UUID,
|
tenant_id: uuid.UUID,
|
||||||
user_id: uuid.UUID,
|
user_id: uuid.UUID,
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ from app.plugins.builtins.unified_search.search_engine import (
|
|||||||
from app.plugins.builtins.unified_search.jobs import (
|
from app.plugins.builtins.unified_search.jobs import (
|
||||||
index_mails,
|
index_mails,
|
||||||
index_contact,
|
index_contact,
|
||||||
index_company,
|
|
||||||
index_event,
|
index_event,
|
||||||
reindex,
|
reindex,
|
||||||
embedding_batch,
|
embedding_batch,
|
||||||
@@ -993,8 +992,8 @@ async def test_index_contact(mock_index_entity, mock_factory, db_session: AsyncS
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@patch("app.plugins.builtins.unified_search.jobs.get_session_factory")
|
@patch("app.plugins.builtins.unified_search.jobs.get_session_factory")
|
||||||
@patch("app.plugins.builtins.unified_search.embedding.index_entity", new_callable=AsyncMock, return_value=True)
|
@patch("app.plugins.builtins.unified_search.embedding.index_entity", new_callable=AsyncMock, return_value=True)
|
||||||
async def test_index_company(mock_index_entity, mock_factory, db_session: AsyncSession):
|
async def test_index_contact_company_type(mock_index_entity, mock_factory, db_session: AsyncSession):
|
||||||
"""index_company calls index_entity."""
|
"""index_contact calls index_entity for company-type contact."""
|
||||||
from app.models.contact import Contact as Company
|
from app.models.contact import Contact as Company
|
||||||
from app.models.tenant import Tenant
|
from app.models.tenant import Tenant
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
@@ -1027,7 +1026,7 @@ async def test_index_company(mock_index_entity, mock_factory, db_session: AsyncS
|
|||||||
sf = async_sessionmaker(bind=db_session.bind, expire_on_commit=False, class_=AsyncSession)
|
sf = async_sessionmaker(bind=db_session.bind, expire_on_commit=False, class_=AsyncSession)
|
||||||
mock_factory.return_value = sf
|
mock_factory.return_value = sf
|
||||||
|
|
||||||
await index_company({}, str(company.id))
|
await index_contact({}, str(company.id))
|
||||||
mock_index_entity.assert_called()
|
mock_index_entity.assert_called()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user