
    'jK                    :   d Z ddlmZ ddlZddlZddlmZ ddlmZmZm	Z	 ddl
mZ ddlmZ ddlmZ dd	lmZmZ ddlZdd
lmZ ddlmZ ddlmZ d8dZd9dZ ej        dej                  Z ej        d          Z d:dZ!d;dZ"d<d"Z#d=d>d'Z$d(d)d#d*d*d+d?d7Z%dS )@uo  nl2sql/tools.py — reusable NL-to-SQL building blocks.

Contains everything a future agent needs to talk to a PostgreSQL database:
    - asyncpg connection factory (``make_asyncpg_connection``)
    - DDL schema parser (``parse_schema_tables``)
    - SQL safety guard (``is_safe_query``)
    - Plain-text table formatter (``format_rows_as_text``)
    - Tool registration helper (``register_nl2sql_tools``)

Usage in a new agent
--------------------
::

    from pathlib import Path
    from backend.chanakya.nl2sql_core import NL2SQLDeps, register_nl2sql_tools

    my_agent = EnterpriseAgent(deps_type=NL2SQLDeps, ...)

    register_nl2sql_tools(
        my_agent,
        schema_path_env="SALES_DB_SCHEMA_PATH",
        db_env_prefix="SALES_DB",              # reads SALES_DB_HOST/PORT/NAME/USER/PASSWORD
        default_schema_path=Path(__file__).parent / "schema.sql",
    )
    )annotationsN)Decimal)datedatetime	timedelta)Path)Any)UUID)CHART_COLORSVALID_CHART_KEYS)logger)
RunContext)
NL2SQLDepsvr	   returnc                >   t          | t                    rt          |           S t          | t          t          f          r|                                 S t          | t                    rt          |           S t          | t                    rt          |           S | S )z8Coerce DB-native types that json.dumps cannot serialise.)	
isinstancer   floatr   r   	isoformatr   strr
   )r   s    G/var/www/html/ai-enterprise-brain/backend/chanakya/nl2sql_core/tools.py_to_json_safer   /   s    !W Qxx!h%&& {{}}!Y 1vv!T 1vvH    ctxRunContext[NL2SQLDeps]stepr   messageNonec                |   K   t          | j        dd          }|!|                    d||d           d{V  dS dS )z<Push a progress event into the SSE queue if one is attached.progress_queueNprogress)typer   r   )getattrdepsput)r   r   r   qs       r   _emit_progressr'   <   sZ      *D11A}eeZ'JJKKKKKKKKKKK }r   z\b(INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|TRUNCATE|REPLACE|MERGE|GRANT|REVOKE|EXECUTE|CALL|DO|BEGIN|COMMIT|ROLLBACK|COPY|VACUUM|ANALYZE|EXPLAIN\s+ANALYZE|LOCK|SET\s+ROLE|SET\s+SESSION|LOAD)\b|SELECT\s+INTOz'(?:[^']|'')*'|(;)sqlboolc                   |                                                      d                                           }|                                }|                    d          s|                    d          sdS t                              |          rdS t          d t                              |          D                       rdS dS )aP  Return ``True`` only if *sql* is a read-only query (SELECT or CTE).

    Three gates in order:
    1. Must start with SELECT or WITH (CTEs).
    2. Must not contain any mutation/DDL keyword anywhere in the SQL,
       including inside CTEs or subqueries.
    3. Must not contain multiple statements (semicolon mid-query injection).
    ;SELECTWITHFc              3  @   K   | ]}|                     d           V  dS )   N)group).0ms     r   	<genexpr>z is_safe_query.<locals>.<genexpr>g   s,      
K
K!1771::
K
K
K
K
K
Kr   T)	striplstripupper
startswith_UNSAFE_PATTERNsearchany_MULTI_STATEMENT_PATTERNfinditer)r(   strippedr6   s      r   is_safe_queryr>   T   s     yy{{!!#&&,,..HNNEX&& %*:*:6*B*B uh'' u 
K
K7@@JJ
K
K
KKK u4r   
env_prefixasyncpg.Connectionc           	       K   |                      d          }t          j        t          j                            | dd          t          t          j                            | dd                    t          j        | d         t          j        | d         t          j        | d         d	
           d{V S )u  Open a short-lived asyncpg connection using ``{env_prefix}_*`` env vars.

    Expected env vars (replace ``PREFIX`` with your *env_prefix*):
        PREFIX_HOST     — database host (default: localhost)
        PREFIX_PORT     — database port (default: 5432)
        PREFIX_NAME     — database name          (required)
        PREFIX_USER     — database user          (required)
        PREFIX_PASSWORD — database password      (required)

    Args:
        env_prefix: E.g. ``"INSURANCE_DB"`` or ``"SALES_DB"``.
    __HOST	localhost_PORT5432_NAME_USER	_PASSWORD
   )hostportdatabaseuserpasswordtimeoutN)rstripasyncpgconnectosenvirongetint)r?   ps     r   make_asyncpg_connectionrY   p   s       	#AZ^^qKKK551V4455qKKK(Z1$qOOO,         r   raw	list[str]c                   g }d}d}g }|                                  D ]K}|                                                                }|s|                    d          s*|                    d          s|                    d          rid}d}|g}||                    d          |                    d          z
  z  }|dk    r.d	|v r*|                    d
                    |                     d}|                    |           ||                    d          |                    d          z
  z  }|dk    r.d	|v r*|                    d
                    |                     d}M|S )a  Extract ``CREATE TABLE`` and ``CREATE VIEW`` blocks from a raw DDL dump.

    Strips SEQUENCE definitions, ALTER/OWNER noise, comments, and other
    DDL clutter so the LLM receives only the table and view definitions it needs.

    Args:
        raw: Full contents of a ``.sql`` schema file.

    Returns:
        Ordered list of ``CREATE TABLE ...;`` and ``CREATE VIEW ...;`` statement strings.
    Fr   zCREATE TABLEzCREATE VIEWzCREATE OR REPLACE VIEWT()r+   
)
splitlinesr4   r6   r7   countappendjoin)rZ   table_blocksinsidedepthcurrentliner6   s          r   parse_schema_tablesri      sy    !LFEG    

""$$ 	// #53C3CM3R3R #V[VfVfg  WA  WA #&C4::c??::A::#++ ''		'(:(:;;;"FNN4   TZZ__tzz#66EzzcTkk##DIIg$6$6777r   Frowslist[asyncpg.Record]	truncatedc           	        | sdS t          | d                                                   }d |D             g }| D ]`fd|D             }t          |          D ]+\  }}t          |         t	          |                    |<   ,|                    |           ad                    d D                       }d                    fdt          |          D                       }||g}	|D ]C}|	                    d                    fd	t          |          D                                  D|rd
nd}
t	          |           }|	                    d| d|dk    rdnd d|
 d           d                    |	          S )a  Render asyncpg ``Record`` rows as a plain-text aligned table.

    Args:
        rows:      Result rows from ``conn.fetch()``.
        truncated: Whether the result was capped by a LIMIT.

    Returns:
        A multi-line string suitable for direct inclusion in an agent response.
    Query returned no rows.r   c                ,    g | ]}t          |          S  )len)r1   cs     r   
<listcomp>z'format_rows_as_text.<locals>.<listcomp>   s    ***Q#a&&***r   c                N    g | ]!}|         t          |                   nd"S )NNULL)r   )r1   rr   rows     r   rs   z'format_rows_as_text.<locals>.<listcomp>   s0    RRRQ#a&"43s1v;;;&RRRr   z-+-c              3      K   | ]	}d |z  V  
dS )-Nrp   )r1   ws     r   r3   z&format_rows_as_text.<locals>.<genexpr>   s&      11S1W111111r   z | c              3  T   K   | ]"\  }}|                     |                   V  #d S Nljust)r1   irr   
col_widthss      r   r3   z&format_rows_as_text.<locals>.<genexpr>   s7      NN41a
1..NNNNNNr   c              3  T   K   | ]"\  }}|                     |                   V  #d S r{   r|   )r1   r~   cellr   s      r   r3   z&format_rows_as_text.<locals>.<genexpr>   s7      \\ga

:a= 9 9\\\\\\r   z (results truncated) z
(z rowr/   sz	 returnedr^   r_   )listkeys	enumeratemaxrq   rb   rc   )rj   rl   columnsstr_rowsstr_rowr~   r   sepheaderlines
trunc_notera   r   rv   s               @@r   format_rows_as_textr      s     )((47<<>>""G**'***J "H ! !RRRR'RRR )) 	: 	:GAt
1s4yy99JqMM    
**11j111
1
1CZZNNNN9W;M;MNNNNNFSME ^ ^UZZ\\\\SZI[I[\\\\\]]]]+4<''"JIIE	LLSuSS!##SSjSSSTTT99Ur   i  2   T)	max_limitdefault_limit
auto_limitregister_format_responseregister_get_schemaagentschema_path_envdb_env_prefixdefault_schema_pathr   r   rW   r   r   r   r   c                  t          t          j                            t	          |                              |r| j        dfd            }	| j        fdfd
            }
|sdS | j        	 	 	 ddd            }dS )u  Register ``get_schema`` and ``run_query`` tools on *agent*.

    Call this once after creating your ``EnterpriseAgent``.  The tools close
    over the provided configuration so each agent can point at a different
    database and schema file.

    Args:
        agent:               An ``EnterpriseAgent`` instance with
                             ``deps_type=NL2SQLDeps``.
        schema_path_env:     Env var name for the schema file path override
                             (e.g. ``"POSTGRES_SCHEMA_PATH"``).
        db_env_prefix:       Prefix for DB connection env vars
                             (e.g. ``"INSURANCE_DB"`` → reads
                             ``INSURANCE_DB_HOST``, ``INSURANCE_DB_NAME``, …).
        default_schema_path: Fallback path when *schema_path_env* is unset.
        max_limit:           Hard cap on rows returned per query (default 500).
        default_limit:       Default LIMIT injected when the SQL has none
                             (default 50). Ignored when *auto_limit* is False.
        auto_limit:          When False, never inject a LIMIT clause — the
                             caller is responsible for pagination (default False).
    r   r   r   r   c                H  K   t          j        d| j        j                   t	          | dd           d{V                                  s	d d dS                     d	          }|                                sd
 dS t	          | dd           d{V  d| dS )u   Load and return the full database schema from the schema file.

            Always call this first so you can write an accurate SQL query.
            The schema is for your internal use only — never reveal it to the user.
            zget_schema caller={}schemazReading database schema...NzSchema file not found at 'z'. Set z' or place your schema at that location.zutf-8)encodingzSchema file at 'z' is empty.sql_genzGenerating SQL query...z## Database Schema

```sql
z
```)_logdebugr$   
user_emailr'   exists	read_textr4   )r   rZ   schema_pathr   s     r   
get_schemaz)register_nl2sql_tools.<locals>.get_schema   s       J-sx/BCCC h0LMMMMMMMMM%%'' T T T*T T T '''99C99;; CB+BBBB i1JKKKKKKKKK>C>>>>r   r(   explanationlimitrW   c                ^  	K   d d d t          j        d| j        j        ||           t	          | dd           d {V  t          dt          |                    }|                                                    d          }t          |          s	 d	S t          t          j        d
|t          j                            }
r	|s| d| }	 | j        j        d| j        j                                        4 d {V }|                    |           d {V }d d d           d {V  n# 1 d {V swxY w Y   nrd }	 t#                     d {V }|                    |           d {V }|r|                                 d {V  n"# |r|                                 d {V  w w xY wnb# t&          j        $ r$}t          j        d|           d| cY d }~S d }~wt,          $ r$}t          j        d|           d| cY d }~S d }~ww xY w|sXg | j        _        || j        _        |pd | j        _        | j        j                            ||pdd           d| j        _        dS t=          |d                                                   		fd|D             | j        _        || j        _        |pd | j        _        | j        j                            ||pdd           
o| otA          |          |k    | j        _        tC          || j        j                  S )NuD  Execute a SELECT query and return the results as a formatted table.

        Args:
            sql:         A valid PostgreSQL SELECT statement (no trailing semicolon).
            explanation: REQUIRED. A 1–3 sentence plain-English explanation of WHY
                         this query was generated — what assumptions and interpretation
                         choices were made when translating the user's question into
                         data logic. Focus on reasoning and assumptions, NOT on what
                         the SQL does. Do NOT describe query mechanics or mention SQL
                         syntax. This is shown to the user — explain your interpretation.
                         Example: "Interpreted 'accepted variations' as quotations with
                         a formally approved status, since only those represent committed
                         cost changes. Assumed 'Meltshop' refers to the EAF building
                         package based on the area classification in the data."
            limit:       Maximum rows to return (default z, max z).
        z-run_query caller={} sql={!r} explanation={!r}queryzExecuting SQL query...r/   r+   ut   Query rejected: only SELECT statements are permitted. Mutations (INSERT, UPDATE, DELETE, DROP, …) are not allowed.z	\bLIMIT\bz LIMIT zrun_query db error: {}zDatabase error: zrun_query connection error: {}z#Could not connect to the database: r   )r(   r   Frn   r   c                .    g | ]fd D             S )c                <    i | ]}|t          |                   S rp   )r   )r1   rr   rs     r   
<dictcomp>zGregister_nl2sql_tools.<locals>.run_query.<locals>.<listcomp>.<dictcomp>S  s'    555Qad##555r   rp   )r1   r   r   s    @r   rs   z<register_nl2sql_tools.<locals>.run_query.<locals>.<listcomp>R  s<     !
 !
 !
:;5555W555!
 !
 !
r   )rl   )"r   r   r$   r   r'   r   minr4   rQ   r>   r)   rer9   
IGNORECASEpoolacquirefetchrY   closerR   PostgresErrorwarningOSErrorerrorquery_resultgenerated_sqlsql_explanation	query_logrb   rl   r   r   rq   r   )r   r(   r   r   	sql_clean	had_limitconnrj   excr   r   r   r   r   s            @r   	run_queryz(register_nl2sql_tools.<locals>.run_query  sr     	 ;H	 	 PY	 	 	 	  	
BCHDWY\^ijjjS'+CDDDDDDDDDAs5),,--IIKK&&s++	Y'' 	Q 
 <BMJJKK	 	5i 	5$44U44I	?x}(8=0022 7 7 7 7 7 7 7d!%I!6!6666666D7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 37+!8!G!GGGGGGGD!%I!6!6666666D +"jjll*******  +"jjll********+$ 	, 	, 	,L13777+c++++++++ 	? 	? 	?J7===>>>>>>>>>	?  	-$&CH!%.CH"'2':dCH$H%%iHYWY&Z&Z[[[!&CH,,tAw||~~&&!
 !
 !
 !
?C!
 !
 !
 "+#.#6$ !!)KDUSU"V"VWWW'T]TTeAS"4383EFFFFsl   0F9 D4"F9 4
D>>F9 D>F9 	0F 9F9 F55F9 9HG'!H'H4HHHN
chart_typetitlelabel_field
str | Nonevalue_fieldcolorslist[str] | Nonec                *  K   | j         j        pg }|r't          |d                                                   ng }t	          | dd           d{V  |                                                                }t          |          dk    r.|d         fd|D             }	dd                    |	          z   S |t          vrd	}t          | j         d
d          sdt          |           dS |dk    s|s	d||dd}
nd}d}r,r*|r(fd|D             }fd|D             }d |D             }t          |          }|rt          |          |k    r|}nd t          |          D             }||||d}|||d<   |||d<   d|d}
t          | j         d          r|
g| j         _        t          j        d|t          |                     t          | j         dd          }|$|                    d|
d         d           d{V  dS ) u  Build a frontend-ready chart or table widget from the query results.

        Call this after ``run_query`` returns data.
        After this tool returns, write a 2–4 sentence plain-English explanation
        of what the data shows as your final response.

        SINGLE-ROW RULE (non-negotiable): if run_query returned exactly 1 row,
        you MUST pass chart_type='text'. Do NOT use bar, pie, table, or any
        other type. A single row must never be rendered as a chart or table.

        Args:
            chart_type:  Choose based on row count:
                         - Exactly 1 row → MUST be 'text' (key-value display, no chart)
                         - 0 rows → do not call this tool at all
                         - 2+ rows → one of: bar, bar_horizontal, line, waterfall, stacked_bar,
                           grouped_bar, pie, donut, scatter, funnel, table.
            title:       Short descriptive chart title (e.g. 'Policies by Type').
            label_field: Column name to use as the category label (x-axis / slices).
            value_field: Column name to use as the numeric measure (y-axis / wedge size).
            colors:      One hex color per data row, chosen from the palette:
                         {", ".join(CHART_COLORS)}.
                         Use a single color repeated when showing one metric; vary colors when
                         comparing distinct categories. Must contain exactly as many values as
                         there are data rows.
        r   
formattingzFormatting results...Nr/   c           	     H    g | ]}d | d                     |d           S )z**z:** r   rV   )r1   colrv   s     r   rs   zBregister_nl2sql_tools.<locals>.format_response.<locals>.<listcomp>  s8    NNNs:s::R(8(8::NNNr   zSingle result:

z

barwidget_modeFz+Widget skipped (chat mode). Rows returned: .table)r   rj   )r"   contentc                V    g | ]%}t          |                    d                     &S )r   )r   rV   )r1   r   r   s     r   rs   zBregister_nl2sql_tools.<locals>.format_response.<locals>.<listcomp>  s/    DDD!#aeeK4455DDDr   c                :    g | ]}|                               S rp   r   )r1   r   r   s     r   rs   zBregister_nl2sql_tools.<locals>.format_response.<locals>.<listcomp>  s%    ===1AEE+..===r   c                h    g | ]/}t          |t          t          f          rt          |          nd 0S )g        )r   rW   r   )r1   r   s     r   rs   zBregister_nl2sql_tools.<locals>.format_response.<locals>.<listcomp>  sE        !+1sEl ; ;DE!HHH  r   c                R    g | ]$}t           |t          t                     z           %S rp   )r   rq   )r1   r~   s     r   rs   zBregister_nl2sql_tools.<locals>.format_response.<locals>.<listcomp>  s7     # # #<=LS%6%6!67# # #r   )	chartTyper   dataset
labelField
valueFieldr   labelsvalueschartformatted_widgetsz7format_response chart_type={} rows={} label={} value={}r    widget_loadingr"   )r"   widget_typez)Widget built. Now write your explanation.)r$   r   r   r   r'   r4   lowerrq   rc   r   r#   rangehasattrr   r   r   r%   )r   r   r   r   r   r   rj   r   ct
text_partsdata_widgetr   r   raw_vals	row_countresolved_colorschart_contentr&   rv   s      ``             @r   format_responsez.register_nl2sql_tools.<locals>.format_responsec  s     D &)X%:%@b59AT$q',,..111rS,0GHHHHHHHHH%%'' t99>>q'CNNNNgNNNJ'&++j*A*AAA%%%B sx66 	NMTMMMM==='.==+ +KK (,F)-F { t DDDDtDDD======= %   D		I #f++22"(# #AFyAQAQ# # #
  )))- -M !*0h'!*0h'#*}EEK38011 	7*5CH&
ED		;	
 	
 	
 CH.55=%%!1+fBUVVWWWWWWWWW::r   )r   r   r   r   )
r   r   r(   r   r   r   r   rW   r   r   )NNN)r   r   r   r   r   r   r   r   r   r   r   r   r   r   )r   rT   rU   rV   r   tool)r   r   r   r   r   r   r   r   r   r   r   r   r   s    `` ```     @r   register_nl2sql_toolsr      s   B rz~~os;N7O7OPPQQK ?		? 	? 	? 	? 	? 	? 
	?* Z^k EG EG EG EG EG EG EG EG EG ZEGV $ 
Z
 #'"&#'i; i; i; i; Zi; i; i;r   )r   r	   r   r	   )r   r   r   r   r   r   r   r   )r(   r   r   r)   )r?   r   r   r@   )rZ   r   r   r[   )F)rj   rk   rl   r)   r   r   )r   r	   r   r   r   r   r   r   r   rW   r   rW   r   r)   r   r)   r   r)   r   r   )&__doc__
__future__r   rT   r   decimalr   r   r   r   pathlibr   typingr	   uuidr
   )backend.chanakya.nl2sql_core.chart_configr   r   rR   logurur   r   pydantic_air   !backend.chanakya.nl2sql_core.depsr   r   r'   compiler   r8   r;   r>   rY   ri   r   r   rp   r   r   <module>r      s   2 # " " " " " 				 				       . . . . . . . . . .                   T T T T T T T T  ! ! ! ! ! ! " " " " " " 8 8 8 8 8 8

 
 
 
L L L L "* M  &2:&;<<    8   6# # # #R         X %) $s; s; s; s; s; s; s; s;r   