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:
Agent Zero
2026-07-23 17:29:53 +02:00
parent 5d79b4f613
commit b15a62bec6
10 changed files with 55 additions and 56 deletions
@@ -263,7 +263,7 @@ def register_context_tools(registry) -> None:
parameters={
"type": "object",
"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"},
"limit": {"type": "integer", "default": 5, "description": "Max results per type"},
},
@@ -300,7 +300,7 @@ def register_context_tools(registry) -> None:
parameters={
"type": "object",
"properties": {
"entity_type": {"type": "string", "description": "contact or company"},
"entity_type": {"type": "string", "description": "contact"},
"entity_id": {"type": "string", "description": "UUID der Entity"},
},
"required": ["entity_type", "entity_id"],
+4 -4
View File
@@ -127,7 +127,7 @@ async def deep_analysis(
contact = contact_result.scalar_one_or_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
comp_result = await db.execute(
@@ -136,10 +136,10 @@ async def deep_analysis(
.where(Company.tenant_id == tid)
.limit(1)
)
company = comp_result.scalar_one_or_none()
extended_context["company"] = _serialize_row(company) if company else None
contact = comp_result.scalar_one_or_none()
extended_context["contact"] = _serialize_row(contact) if contact else None
# All mails for company
# All mails for contact
mail_result = await db.execute(
select(Mail)
.where(Mail.contact_id == eid)
+21 -21
View File
@@ -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.notifications import create_notification
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.plugins.builtins.ai_proactive.models import (
ContextLog,
@@ -178,28 +178,28 @@ async def gather_context(
)
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(
select(ContactPerson)
.where(ContactPerson.contact_id == entity_id)
.where(ContactPerson.tenant_id == tenant_id)
.limit(5)
)
companies: list[dict[str, Any]] = []
contacts_list: list[dict[str, Any]] = []
for cc in cc_result.scalars().all():
comp_result = await db.execute(
select(Company)
.where(Company.id == cc.company_id)
.where(Company.tenant_id == tenant_id)
select(Contact)
.where(Contact.id == cc.contact_id)
.where(Contact.tenant_id == tenant_id)
.limit(1)
)
comp = comp_result.scalar_one_or_none()
if 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
companies.append(comp_data)
context["company"] = companies[0] if companies else None
contacts_list.append(comp_data)
context["contact"] = contacts_list[0] if contacts_list else None
context["companies"] = companies
# Upcoming calendar events
@@ -262,25 +262,25 @@ async def gather_context(
if mail and mail.contact_id:
comp_result = await db.execute(
select(Company)
.where(Company.id == mail.contact_id)
.where(Company.tenant_id == tenant_id)
select(Contact)
.where(Contact.id == mail.contact_id)
.where(Contact.tenant_id == tenant_id)
.limit(1)
)
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(
select(Company)
.where(Company.id == entity_id)
.where(Company.tenant_id == tenant_id)
select(Contact)
.where(Contact.id == entity_id)
.where(Contact.tenant_id == tenant_id)
.limit(1)
)
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(
select(ContactPerson)
.where(ContactPerson.contact_id == entity_id)
@@ -297,12 +297,12 @@ async def gather_context(
contact = contact_result.scalar_one_or_none()
if 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
contacts.append(contact_data)
context["contacts"] = contacts
# Mails for this company
# Mails for this contact
from app.plugins.builtins.mail.models import Mail
mail_result = await db.execute(
+1 -1
View File
@@ -76,7 +76,7 @@ class CalendarEntry(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"
__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)
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:
"""Index a calendar event: generate and store embedding."""
from sqlalchemy import text
@@ -18,7 +18,7 @@ DEFAULT_LLM_MODEL = os.environ.get('SEARCH_LLM_MODEL', 'ollama/deepseek-v4-flash
QUERY_ANALYZE_SYSTEM = (
"Du bist ein Query-Analyzer fuer ein CRM. "
"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": {}}'
)
+1 -1
View File
@@ -17,7 +17,7 @@ router = APIRouter(prefix="/api/v1/addresses", tags=["addresses"])
@router.get("")
async def list_addresses(
entity_type: str = Query(..., pattern="^(company|contact)$"),
entity_type: str = Query(..., pattern="^contact$"),
entity_id: str = Query(...),
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(require_permission("addresses:read")),
+1 -1
View File
@@ -321,7 +321,7 @@ async def _execute_api_action(
return {"error": f"Unsupported entity: {entity}", "status_code": 400, "success": False}
async def _exec_contacts\(
async def _exec_contacts(
db: AsyncSession,
tenant_id: uuid.UUID,
user_id: uuid.UUID,